General Purpose I/O: Unterschied zwischen den Versionen

Aus C und Assembler mit Raspberry
Die Seite wurde neu angelegt: „General Purpose I/O“
 
 
(3 dazwischenliegende Versionen desselben Benutzers werden nicht angezeigt)
Zeile 1: Zeile 1:
General Purpose I/O
In diesem Kapitel gehe ich auf die GPIO-PINs ein, die der Raspberry Pi für die Kommunikation mit der Außenwelt verwenden kann. Idealerweise ist ein PIN mit einer LED, die auf dem Board verbaut ist, verbunden, so dass wir zunächst auf externe Hardware verzichten können und effektiv ein Ergebnis zu sehen. Unter den Baremetal-Programmieren kommt dies einem “HelloWorld” Programm am nächsten. Im ersten Unterkapitel werden wir die LED blinken lassen, im nächsten versuchen wir etwas Struktur in den Sourcecode zu bekommen und dann den Code soweit verbessern, dass die Funktionen universaler genutzt werden können.
 
Der Raspberry Pi besitzt einige GPIO, die gerade für Hardware Freunde interessant sind, da diese einfach zu programmieren sind und sich gut mit der Außenwelt verbinden lassen. Auf dem Raspberry Pi 4 gibt es 40 GPIO-Pins, von denen 26 Pins als Ausgang oder Eingang programmiert werden können.
Hier mal eine Zusammenfassung der 40 Pins und deren Zuordnung:
 
[[Datei:Raspi1.png|rand|600x600px]]
 
Zusätzlich gibt es noch weitere interne GPIO-Adressen, die auch über die Programmierschnittstelle programmiert werden können. Eine davon ist die grüne LED, die über den Port 42 auf dem Raspberry Pi 4 angesteuert werden kann.
 
== LED leuchten lassen ==
Wie bereits zuvor erwähnt, gibt es auf dem Raspberry Pi 4 eine grüne LED, die wir per Programm ansprechen können und diese an- beziehungsweise abschalten können. Dies wird über den Port 42 gesteuert. Bei den älteren Modellen ist die Adresse eine Andere. Schau bitte im Anhang, welche das sind. Im Kapitel “Erweiterte Funktionen für die LED” ist es sehr einfach, auch dort dann die entsprechende LED anzusteuern, aber vergesse dann nicht, auch die Basis-Adresse des Raspberrys im “base.inc” zu ändern.
 
[[Datei:Raspi2.png|rand|600x600px]]
 
Zunächst der komplette Sourcecode:
<syntaxhighlight lang="asm">
.equ RPI_BASE, 0xFE000000
 
.equ GPIO_BASE, RPI_BASE + 0x200000
 
@ GPIO function select (GFSEL) registers have 3 bits per GPIO
.equ GPFSEL0, GPIO_BASE + 0x0  @GPIO select 0
.equ GPFSEL1, GPIO_BASE + 0x4  @GPIO select 1
.equ GPFSEL2, GPIO_BASE + 0x8  @GPIO select 2
.equ GPFSEL3, GPIO_BASE + 0xC  @GPIO select 3
.equ GPFSEL4, GPIO_BASE + 0x10  @GPIO select 4
 
@GPIO SET/CLEAR registers have 1 bit per GPIO
.equ GPSET0, GPIO_BASE + 0x1C @set0 (GPIO 0 - 31)
.equ GPSET1, GPIO_BASE + 0x20 @set1 (GPIO 32 - 63)
.equ GPCLR0, GPIO_BASE + 0x28 @clear0 (GPIO 0 - 31)
.equ GPCLR1, GPIO_BASE + 0x2C @clear1 (GPIO 32 - 63)
</syntaxhighlight>
 
Dieser Teil des Sourcecodes wird in unsere Ausgangsdatei zwischen den Zeilen 4 und 5 eingefügt. Dieser Teil erzeugt Variablen, die während der Kompilierung für den Assembler verfügbar sind.
 
<syntaxhighlight lang="asm">
/*
* LED is Pin42
* on the GPFSEL4 register Bits 8-6
* 001 = GPIO Pin 42 is an output
*/
 
  mov r1,#1
  lsl r1,#6  /* -> 001 000 000 */
 
/*
* 0x10 GPFSEL4 GPIO Function Select 4
*/
  ldr r0,=GPFSEL4
  str r1,[r0]
 
/*
* Set the 42 Bit
* 25:0 SETn (n=32..57) 0 = No effect; 1 = Set GPIO pin n.
* 42 - 32 = 10
*/
 
  mov r1,#1
  lsl r1,#10
 
MainLoop:        @Endlosschleife
 
/*
* 0x2C GPCLR1 GPIO Pin Output Clear 1
*/
 
  ldr r0,=GPCLR1 @LED ist aus
  str r1,[r0]
 
/*
* Waiting
*/
 
  mov r2,#0x3F0000
wait1$:
  sub r2,#1
  cmp r2,#0
  bne wait1$
 
/*
* 0x20 GPSET1 GPIO Pin Output Set 1
*/
 
  ldr r0,=GPSET1 @LED ist an
  str r1,[r0]
 
/*
* Waiting
*/
 
  mov r2,#0x3F0000
wait2$:
sub r2,#1
cmp r2,#0
bne wait2$
 
  b MainLoop
</syntaxhighlight>
Und das eigentliche Programm.
 
Wenn du jetzt keine Lust hast, das alles abzuschreiben, kannst du den Source auch über [https://www.satyria.de/arm/source/kurs/zweites.s zweites.s] herunterladen.
 
Damit du nun auch siehst, was da passiert. Kompiliere es nach dem oben genannten Schema. Ersetze “erstes” durch “zweites” und folge den Anweisungen, wie oben beschrieben.
Die beschriebene LED fängt nach einem Boot an zu blinken.
 
Zunächst beschreibe ich die neuen Befehle, die ich hier verwendet habe und im Anschluss beschreibe ich das Programm, wie es funktioniert.
 
== Neue Befehle ==
=== .equ ===
'''.equ''' selbst ist kein Assemblerbefehl für den Prozessor. Dieser gibt dem Assembler selbst eine Anweisung. '''.equ''' sagt dem Assembler, dass er eine Variable erzeugen soll mit einem bestimmten Inhalt. Dies ermöglicht dem Programmierer statt für bestimmte Zahlenwerte, einen Variablennamen zu verwenden. Alternativ könnte auch der Befehl '''.set''' verwendet werden, der die gleiche Funktion hat.
Als Beispiel:
 
<syntaxhighlight lang="asm">
.equ RPI_BASE, 0xFE000000
</syntaxhighlight>
 
Hier wird der Variablen “RPI_BASE” die Zahl “0xFE000000” zugewiesen. Aber auch rechnen kann man hier.
 
<syntaxhighlight lang="asm">
.equ GPIO_BASE, RPI_BASE + 0x200000
</syntaxhighlight>
 
Dies bedeutet, das der Wert aus “RPI_BASE” mit dem Wert “0x200000” addiert und ergibt dann “0xFE200000”.
 
Aber was bedeuten diese Werte?
Der Raspberry Pi verwendet Adressregister, die bestimmte Funktionen übernehmen. Diese werden an einer bestimmten Adresse im Speicher des Raspberry Pi angeboten, die wir benutzen dürfen. Im Dokument [https://www.raspberrypi.org/documentation/hardware/raspberrypi/bcm2711/rpi_DATA_2711_1p0.pdf BCM2711 ARM Peripherals] werden diese Register beschrieben und wie diese anzuwenden sind.
Der erste Wert (RPI_BASE), den wir festlegen, ist die Grundlegende Adresse, der Peripherie, wie sie für den ARM sichtbar ist (Seite 10). Für den Raspberry Pi4 ist das die Adresse 0xFE000000. Auf den älteren Modellen ist es eine andere. Im Anhang “Basisadressen der Modelle” habe ich diese aufgelistet. Wenn nun ein anderes Model verwenden wird, muss genau hier, diese Basisadresse angepasst werden.
Um die LED anzusteuern verwenden wir das GPIO-Register. Laut Dokumentation (Seite 83) wäre es “0x215000”. Durch Tests kann ich aber sagen, dass es so nicht stimmen kann. In den Vorgängermodellen war es die Adresse “0x200000” und diese funktioniert auch auf dem Raspberry Pi 4. Leider gibt es in den Dokumenten einige Fehler und im Zweifel hilft hier die Community für den Raspberry Pi.
Die Restlichen Werte sind aus dem Kapitel 5.2. des Dokuments herausgeholt und als Variablen definiert.
 
=== Register ===
Kommen wir zu etwas Grundsätzlichen. In einem Prozessor gibt es Register, mit denen ein Prozessor schnell rechnen kann. Auf dem ARM gibt es hierfür 16 Stück, '''R0'''-'''R15''' und '''CPSR'''. Die Register '''R13'''-'''R15''' haben allerdings auch andere Funktionen. '''R13''' wird auch das Stabelregister ('''SP'''), '''R14''' Rücksprungadresse ('''LR''') und '''R15''' als Programmzeiger ('''PC''') genannt. In der Regel sollte man diese drei Register auch nur für diese Zwecke verwenden.
'''CPSR''' ist das Statusregister, welches auch '''APSR''' genannt werden kann. Dies enthält eine Kopien der Statusflags der Arithmetic Logic Unit (ALU). Sie werden auch als Bedingungscode-Flags bezeichnet. Der Prozessor verwendet sie, um zu bestimmen, ob bedingte Anweisungen ausgeführt werden sollen oder nicht.
 
{| class="wikitable"
 
|-
| R0 || R1 || R2 || R3 || R4 || R5 || R6 || R7 || R8 || R9 || R10 || R11 || R12 || R13<br />(SP) || R14<br />(LR) || R15<br />(PC) || CPSR
|}
 
=== ldr (Load Register) ===
Der Befehl '''ldr''' holt eine 32-Bit Zahl aus dem Speicher und schreibt sie in ein Register.
 
<syntaxhighlight lang="asm">
ldr r0,=GPFSEL4
</syntaxhighlight>
 
In unserem Fall weisen wir '''r0''' die Zahl aus GPFSEL4 (0xFE200010) zu. Allerdings kann der ARM dies nicht direkt, wie es hier steht. Hier benutzen wir eine Funktion des Assemblers, der uns im Speicher diese Zahl ablegt und der Assembler diese Adresse dort einträgt.
Vom Grunde her, könnten wir dies genauso tun, in dem wir selbst diese Adresse erstellen und darauf verweisen. In einem späteren Teil des Kurses wird das auch vorkommen.
 
=== lsl (Logical Shift Left) ===
Mit '''lsl''' kann man binär Bits verschieben und in diesem Fall werden die Bit nach links verschoben.
 
Als Beispiel aus unserem Code:
 
<syntaxhighlight lang="asm">
lsl r1,#6
</syntaxhighlight>
 
Hier wird der Inhalt aus '''r1''' um 6 Stellen nach links verschoben.
 
Wenn wir zum Beispiel '''r1''' mit “0x1” belegen, so sieht diese Zahl als Binärcode folgendermaßen aus:
<syntaxhighlight lang="shell">
0000 0000 0000 0000 0000 0000 0000 0001
</syntaxhighlight>
Wenn wir nun wie oben beschrieben um 6 nach links verschieben, wird aus der Zahl folgender Binärcode:
<syntaxhighlight lang="shell">
0000 0000 0000 0000 0000 0000 0010 0000
</syntaxhighlight>
 
=== sub (Subtrahieren) ===
Wie der Befehl schon sagt, subtrahiert man hier etwas.
 
<syntaxhighlight lang="asm">
sub r2,#1
</syntaxhighlight>
 
In unserem Beispiel wird der Inhalt von Register '''r2''' mit 1 subtrahiert.
 
=== cmp (Compare) ===
Mit dem Befehl '''cmp''' werden Daten verglichen und das Register '''CPSR''' mit dem Ergebnis gefüllt. Wir verwenden es bereits im nächsten Befehl.
 
<syntaxhighlight lang="asm">
cmp r2,#0
</syntaxhighlight>
 
Hier im Beispiel, wird '''r2''' mit "0" verglichen.
 
=== bne (Branch wenn nicht gleich) ===
Den Befehl '''b''' haben wir bereits im vorigen Teil des Kurses erwähnt. '''b''' bedeute, dass an eine andere Adresse gesprungen werden soll. Hier kommt nun eine Besonderheit des ARM zum Einsatz. Der ARM kann die meisten seiner Befehle mit Bedingungen ausführen, die über das Statusregister gesteuert werden. Es gibt einige Bedingungen, die im nächsten Abschnitt beschrieben werden.
 
<syntaxhighlight lang="asm">
bne wait1$
</syntaxhighlight>
 
In unserem Beispiel wird der Sprung nur durchgeführt, wenn das Ergebnis des vorigen Befehls nicht gleich war. Sollte das Ergebnis gleich gewesen sein, wird der Befehl ignoriert und die nächste Anweisung ausgeführt.
 
=== Bedingungscode ===
 
Die meisten Befehle des ARMs können mit einem Bedingungscode versehen werden. Möglich sind folgende Suffixe, die einfach an den entsprechenden Befehl angehängt werden:
 
{| class="wikitable"
|+
!Suffix
!Bedeutung
|-
|EQ
|Gleich
|-
|NE
|Nicht gleich
|-
|CS oder HS
|Größer oder gleich (ohne Vorzeichen)
|-
|CC oder LO
|Kleiner (ohne Vorzeichen)
|-
|MI
|Negativ
|-
|PL
|Positiv oder Null
|-
|VS
|Bei Überlauf
|-
|VC
|Kein Überlauf
|-
|HI
|Größer (ohne Vorzeichen)
|-
|LS
|Kleiner oder gleich (ohne Vorzeichen)
|-
|GE
|Größer oder gleich (ohne Vorzeichen)
|-
|LT
|Kleiner (mit Vorzeichen)
|-
|GT
|Größer (mit Vorzeichen)
|-
|LE
|Kleiner oder gleich (mit Vorzeichen)
|-
|AL
|Immer. Wird in der Regel nicht geschrieben
|}
 
== Das Programm (zweites.s) ==
Unser Programm hat folgenden Ablauf:
* Raspberry Pi darauf vorbereiten, dass er mit eine LED kommunizieren soll
* LED Ausschalten
* Warten
* LED Anschalten
* Warten
* Schleife ausführen
 
=== Raspberry Pi vorbereiten ===
 
Zunächst müssen wir dem Raspberry mitteilen, was wir vorhaben. Dazu müssen wir dem Register GPFSEL mitteilen, wie er seine Pins verwendet soll. Welche Register hier gemeint sind, habe ich bereits unter [[Systemprogrammierung / Bare Metal]] beschrieben. Laut Dokumentation im Kapitel 5 gibt es hierfür fünf GPFSEL Register, die für verschiedene GPIO-Adressen zuständig sind. GPFSEL0 ist für die GPIO-Adressen 0-9, GPFSEL1 für 10-19 usw. zuständig. Da unsere LED über GPIO-Adresse 42 angesprochen wird, müssen wir GPFSEL4 verwenden.
Innerhalb des Registers hat jede GPIO-Adresse 3 Bits zur Verfügung um deren Funktion zu definieren oder aber auch abfragen zu können. Diese drei Bits sind wie folgt definiert:
 
{| class="wikitable"
|+ Status des GPIO-Registers
|-
| 000 || PIN ist ein Eingang
|-
| 001 || PIN ist ein Ausgang
|-
| 100 || PIN hat die alternative Funktion 0
|-
| 101 || PIN hat die alternative Funktion 1
|-
| 110 || PIN hat die alternative Funktion 2
|-
| 111 || PIN hat die alternative Funktion 3
|-
| 011 || PIN hat die alternative Funktion 4
|-
| 010 || PIN hat die alternative Funktion 5
|}
 
Als Status werden nur zwei Zustände fest angegeben. Dies sind die Status, den Pin entweder als Eingang oder als Ausgang zu definieren. Alle anderen Status werden einer alternativen Funktion zugeschrieben. Hierbei bedeutet die alternative Funktion des einen PINs nicht die gleiche Funktion des anderen Bits. Dazu kann in der Dokumentation im Kapitel 5.3 “Alternative Function Assignments” geschaut werden, welche es da gibt. Später, werden wir diese auch verwenden.
Da wir nun eine LED ansteuern wollen, müssen wir hier die Funktion “001 - PIN ist ein Ausgang” verwenden.
Im Register GPFSEL4 sind die drei Bits 8-6 zuständig für den Pin 42. Also hier müssen wir die Bitfolge “001” eintragen. Eine Möglichkeit wäre, diesen Bitcode einfach direkt reinzuschreiben “0b001000000”, aber da wir etwas in Zukunft blicken und auch etwas mehr Code verstehen wollen, setzen wir einfach '''r1''' auf 1 (entspricht wie 0b001) und schieben es mit dem Befehl '''lsl''', den Inhalt, um 6 Stellen nach links. Das Ergebnis ist das gleiche.
 
<syntaxhighlight lang="asm">
mov r1,#1  @ entspricht 0b001
lsl r1,#6  @ -> 0b001 000 000
</syntaxhighlight>
 
Die Funktionsregister sind nichts anderes als Adressen, in denen bestimmte Werte abgelegt sind. Die Peripherie nimmt dort diese Werte heraus und reagiert entsprechend.
Um nun unseren Wert an die richtige Stelle zu schreiben, müssen wir zunächst die Adresse des Funktionsregisters in ein Register laden und dann über dieses Register unseren Wert übertragen.
 
<syntaxhighlight lang="asm">
ldr r0,=GPFSEL4 @Lade Adsresse von GPFSEL4 nach r0
str r1,[r0]    @Speichere r1 in diese Adresse
</syntaxhighlight>
 
Es ist mit dem ARM-Prozessor nicht möglich, direkt in einen Speicherbereich einen Wert abzulegen. Alle Befehle des ARM-Prozessors besteht aus 32-Bits und können nicht erweitert werden, wie manch anderer Prozessor es könnte. In solch einem 32-Bit-Code steht der Befehl selbst, welche Register verwendet werden, der Bedingungscode usw. übrig bleiben da wenige Bits, die eine Adresse speichern könnten. Kurze Wege wären bei einer Adressierung relativ zum Befehl selbst möglich. Da aber das Register sehr wahrscheinlich außerhalb dieses Bereiches ist, müssen wir einen Umweg gehen. Der Befehl '''ldr''' lädt eine Adresse einer Variablen, die zu diesem Befehl Relativ entfernt ist in ein Register. Also hier wird einfach nur ein Wert eingetragen, der besagt, dass die Adresse XYZ um z.B. 200 Bytes weiter abgelegt ist. In unserem Beispiel wird der Inhalt der Adresse der Variablen “GPFSEL4” nach '''r0''' geladen, also der Wert aus GPFSEL4 selbst.
Mit dem nächsten Befehl '''str''' (store) speichern wir den Inhalt des Registers '''r1''' in die Adresse aus '''r0'''. Damit haben wir dem Funktionsregister “GPFSEL4” mitgeteilt, wie es mit unserem Pin umgehen muss.
===LED An- und Ausschalten===
Um nun unser LED an- beziehungsweise abzuschalten verwenden wir wiederum zwei Register, die uns der Raspberry Pi anbietet. Das sind die Register "GPSET" und "GPCLR". Diese können bestimmte PINs im GPIO setzen oder löschen. Laut Dokumentation auf Seite 89 sind bestimmte Bits für bestimmte PINs zuständig. Schauen wir uns zunächst GPSET an. GPSET0 ist für die PINs 0-31 zuständig und GPSET1 für die PINs 32-57.
Also unser PIN, den wir brauchen wird mit GPSET1 angesprochen. Damit wir die Position auch richtig berechnen, schreiben wir hier wieder eine 1 ins Register '''r1''' und schieben den Bit um 10 (42 - 32 = 10) Stellen nach links. Da diese Regel auch für GPCLR1 gilt, benutzen wir später diesen Wert aus dem Register '''r1''' auch dort.
Das eigentliche Register "GPSET1" verwenden wir später in unseren Endlosschleife.
 
<syntaxhighlight lang="asm">
mov r1,#1
lsl r1,#10
</syntaxhighlight>
 
===Die Endlosschleife===
Nun kommen wir zu unseren Schleife. Diese bezeichnen wir einfach als "MainLoop" und schreiben ein entsprechendes Label.
 
<syntaxhighlight lang="asm">
MainLoop:
</syntaxhighlight>
 
Wie bereits zuvor beschrieben, werden wir das Register GPCLR1 verwenden, um zunächst unsere LED auszuschalten. Dazu laden wir wieder mit '''ldr''' die Adresse des Registers für GPCLR1 nach '''r0''' und speichern dann '''r1''' (Unser PIN) in dieses Register.
 
<syntaxhighlight lang="asm">
ldr r0,=GPCLR1 @LED is off
str r1,[r0]
</syntaxhighlight>
 
Wenn wir nun die LED jetzt sofort wieder anschalten, so würden wir den Effekt nicht erkennen, da dies so schnell von statten gehen würde. Damit wir es aber erkennen können, müssen wir den Prozessor erstmal etwas beschäftigen.
Dazu erstellen wir eine Schleife (''wait1$''). Dort setzen wir das Register '''r2''' auf einen Wert von 0x3F0000. Innerhalb der Schleife reduzieren wir den Wert um 1 und prüfen, ob '''r2''' gleich Null ist. Solange dies nicht der Fall ist, lassen wir einfach die Schleife nochmal durchlaufen. Sobald es erreicht wurde, läuft das Programm weiter.
 
<syntaxhighlight lang="asm">
  mov r2,#0x3F0000  @Setze r2 auf 3F0000
wait1$:
  sub r2,#1          @Subtrahiere von r2 #1
  cmp r2,#0          @Vergleiche den Wert mit "0"
  bne wait1$        @Wenn es nicht gleich ist, wiederhole die Schleife
</syntaxhighlight>
 
Im nächsten Schritt verwenden wir das Register GPSET1 auf die gleiche weise, wie zuvor das GPCLR1 Register. Damit wird die LED eingeschaltet.
 
<syntaxhighlight lang="asm">
ldr r0,=GPSET1 @LED is on
str r1,[r0]
</syntaxhighlight>
 
Und wir warten wieder eine Zeitlang mit der Schleife wait2$.
 
<syntaxhighlight lang="asm">
  mov r2,#0x3F0000  @Setze r2 auf 3F0000
wait2$:
  sub r2,#1          @Subtrahiere von r2 #1
  cmp r2,#0          @Vergleiche den Wert mit "0"
  bne wait2$        @Wenn es nicht gleich ist, wiederhole die Schleife
</syntaxhighlight>
 
Nun springen wir in unsere Endlosschleife und die LED blinkt.
 
<syntaxhighlight lang="asm">
b MainLoop
</syntaxhighlight>
 
==Includes verwenden==
Wenn wir unseren Code etwas anschauen, werden wir bereits etwas feststellen. Wenn unser Programm etwas mehr können soll, als nur eine LED an und ab zu schalten, werden wir irgendwann zu einem Code kommen, der sehr unübersichtlich wird. Der Compiler bietet uns hierfür die Möglichkeit an, bestimmten Code auszulagern.
Unser jetziger Code besteht zur Zeit aus zwei Teilen, einen Deklarationsbereich und dem eigentlichen Code. Um nun zunächst mal das prinzip der "Includes" zu zeigen, werden wir unsere Deklarationen in eine andere Datei auslagern.
Am besten ist es auch, diese Datei dann auch so zu gestalten, dass wir immer wieder darauf zugreifen können, egal was wir dann schreiben. Die Deklarationen für den Raspberry Pi bleiben immer gleich und kann dann in anderen Sourcecodes verwendet werden.
 
Also, das erste Include, welches wir erzeugen, wird eine Declarations-Datenbank werden.
Wir nennen sie einfach “base.inc” (Basis-Daten).
 
<syntaxhighlight lang="asm">
/* Raspberry PI 4 Deklarationen
*  Datei: base.inc
*  Erstellt: 17.10.2020
*/
 
.equ RPI_BASE, 0xFE000000
 
.equ GPIO_BASE, RPI_BASE + 0x200000
 
@ GPIO function select (GFSEL) registers have 3 bits per GPIO
.equ GPFSEL0, GPIO_BASE + 0x0  @GPIO select 0
.equ GPFSEL1, GPIO_BASE + 0x4  @GPIO select 1
.equ GPFSEL2, GPIO_BASE + 0x8  @GPIO select 2
.equ GPFSEL3, GPIO_BASE + 0xC  @GPIO select 3
.equ GPFSEL4, GPIO_BASE + 0x10  @GPIO select 4
 
@GPIO SET/CLEAR registers have 1 bit per GPIO
.equ GPSET0, GPIO_BASE + 0x1C @set0 (GPIO 0 - 31)
.equ GPSET1, GPIO_BASE + 0x20 @set1 (GPIO 32 - 63)
.equ GPCLR0, GPIO_BASE + 0x28 @clear0 (GPIO 0 - 31)
.equ GPCLR1, GPIO_BASE + 0x2C @clear1 (GPIO 32 - 63)
</syntaxhighlight>
 
In unserem Hauptprogramm können wir diese Datei einfach per ".include" hinzufügen. Intern sieht der Assembler diese so an, als wäre das ".include" durch den Code aus dem Include ersetzt. Bei Fehlermeldung gibt uns der Assembler allerdings auch die entsprechende Fehlerzeile aus dem Include wieder, so dass wir relativ schnell sehen, wo den der Fehler ist.
 
<syntaxhighlight lang="asm">
.include "base.inc"
</syntaxhighlight>
 
Den Sourcecode gibt es nun als Zip-Datei, da unsere Projekte ab sofort mehrer Dateien enthalten.
[https://www.satyria.de/arm/source/kurs/2.2.zip 2.2.zip]
<br><br>
Kompiliert wird das Programm wie zuvor. Es hat sich soweit nichts verändert.
Aber wir machen es noch besser und verwendet nun Funktionen.
 
==Funktionen verwenden==
Funktionen werden verwendet, wenn man zum Beispiel immer wiederkehrende Dinge erledigen möchte. Aber auch, um verschiedene Dinge aus dem Hauptprogramm herauszulösen, um die Lesbarkeit zu verbessern.
Als Beispiel zeige ich hier eine Funktion, die wir so jetzt nicht verwenden werden, aber zur Erklärung durchaus Vorteilhaft ist.
Wir erstellen uns nun eine Funktion mit dem Namen “Get_GPIO_Base”.
 
<syntaxhighlight lang="asm">
/*
*  int (r0) Get_GPIO-Base{void}
*  gibt die GPIO Adresse in r0 zurück
*/
Get_GPIO_Base:
  push {lr}
  ldr r0,=GPIO_BASE
  pop {pc}
</syntaxhighlight>
 
Da Funktionen in der Regel auch für andere Programme verwendet werden, sollte man jeder Funktion eine Beschreibung beifügen, so dass man später jederzeit nachvollziehen kann, für was den die Funktion verwendet wird. Genauso ist es wichtig, was erwartet die Funktion an Daten und welche bekomme ich zurück, wenn ich sie Aufrufe.
 
Diese Funktion wird die GPIO-Adresse in '''r0''' zurückgeben und verlangt keine Parameter.
 
Über ein Label wird die Funktion deklariert. In unserem Fall bekommt die Funktion, den Namen Get_GPIO_Base. Dies ist dann auch der Name, mit der wir später diese Funktion aufrufen können.
 
Wenn wir eine Funktion aufrufen, wird der Funktion eine Adresse im '''lr''' Register übergeben. Dies ist die Adresse, von der die Funktion aufgerufen wurde, plus einem Offset, der den nächsten Befehl angibt. Damit wir später wieder zurückkehren können, müssen wir diese Adresse sichern. Dies erfolgt mit dem Befehl "push". Dieser Befehl schreibt die Adresse in den Stack (siehe auch neue Befehle für Funktionen).
Wenn nun unsere Ausführungen der Funktion fertig ist, setzen wir einfach die Adresse mit "pop" in den Programmzähler ('''pc''') und unser Programm, welches diese Funktion aufgerufen hat, wird fortgesetzt.
Unsere Funktion ist nun soweit fertig, so dass wir sie aus unserem Hauptprogramm aufrufen können.
 
<syntaxhighlight lang="asm">
bl Get_GPIO_Base
</syntaxhighlight>
 
Mit einem einfachen "b" (branch) wäre es möglich unsere Funktion aufzurufen. Allerdings wird mit einem einfachen "branch" der Zeiger nicht an an lr übergeben. Damit wüsste die Funktion später nicht mehr, von wo den ich diese Funktion aufgerufen habe. Mit dem erweiterten Befehl bl (branch with link) wird diese Adresse ins '''lr''' Register geschrieben. In der Funktion wird die Adresse später verwendet, um den nächsten Befehl, der hier folgt, auszuführen.
 
Wie zuvor berichtet, ist das nur ein Beispiel, hat aber für uns überhaupt keinen Sinn. Dies sollte nur verdeutlichen, wie Funktionen geschrieben werden und funktionieren. In den folgenden Beispielen, verwenden wir diese Art von Funktionen, allerdings gibt es noch einiges zu beachten, welche ich hier nun erläutern werde.
 
===Quasi Standard für Register in Funktionen===
Problematisch bei Funktionen ist, dass der Benutzer solcher Funktionen nicht weiß, welche Register in der Funktion verwendet werden. Dies kann zu Komplikationen führen, die so nicht gewollt sind. Man kann sich das gut vorstellen, wenn ich in meinem Programm das Register r5 verwende, um zum Beispiel darin meine Berechnung abzulegen, die ich später weiterhin verwenden will, sollte der Wert auch weiterhin verfügbar sein. Wenn allerdings die Funktion dieses Register verwendet, wird dieses Register überschrieben und mein Wert ist weg. Damit das aber nicht passiert, werden hierfür Regeln aufgestellt, so dass man sich darauf verlassen kann, dass bestimmte Register nicht überschrieben werden oder zumindest nach Rückkehr aus der Funktion, die entsprechenden Register wieder ihren Wert haben.
 
Assemblerfunktionen haben auch in anderen Hochsprachen ihre Daseinsberechtigung. Auch dort können solche Funktionen aufgerufen werden. Da die Hochsprache hier auch nicht weiß, was den in der Funktion passiert, muss hier auch auf Richtlinien geachtet werden.
 
Einige Programmierer haben dazu einen Standard entwickelt, an den wir uns hier auch halten werden. Dieser Standard hat den Namen “Application Binary Interface (ABI)”. Dort wurde für den AMD-Prozessor einige Richtlinien festgelegt:
 
Der Standard besagt, dass '''r0''', '''r1''', '''r2''' und '''r3''' der Reihe nach als Eingaben für eine Funktion verwendet werden können. Wenn eine Funktion keine Eingaben benötigt, spielt es keine Rolle, welche Werte diese beinhalten. Als Rückgabe wird immer das Register '''r0''' verwendet. Wenn eine Funktion keine Rückgabe hat, spielt es keine Rolle, welchen Wert '''r0''' annimmt.
Außerdem muss '''r4''' bis '''r12''' nach dem Ausführen einer Funktion dieselben Werte besitzen, wie zuvor beim Start der Funktion. Dies bedeutet, dass man beim Aufrufen einer Funktion sicher sein kann, dass sich der Wert von '''r4''' bis '''r12''' nicht ändert, aber bei '''r0''' bis '''r3''' kann man sich nicht sicher sein.
 
Wenn man dennoch Register höher als '''r3''' innerhalb der Funktion benötigt, müssen die Werte gesichert werden und am Ende der Funktion wieder hergestellt werden. Um dies zu erfüllen, verwenden wir den Stapel, den wir am Anfang des Hauptprogramms festgelegt haben und verwenden dazu die Befehle '''push''' und '''pop''', die uns diese Arbeit abnehmen.
 
Beispiel:
 
<syntaxhighlight lang="asm">
push {r5,lr}
</syntaxhighlight>
 
Dieser Befehl legt die Register lr und r5 im Stapel ab.
Mit
 
<syntaxhighlight lang="asm">
pop {r5,pc}
</syntaxhighlight>
 
werden die Daten aus dem Stapel wieder geladen. Hierbei ist zu beachten, dass der Zeigers des Stapels auch angepasst wird und die Daten nicht mehr verfügbar sind. Hier im Beispiel wird mit '''push''' '''lr''' auf den Stabel gelegt und dieser Wert mit '''pop''' ins '''pc''' Register gelegt.
 
===Neue Befehle die wir für Funktionen verwenden===
====bl (branch with link)====
bl ist ein Branch Befehl, der einen Link mit zur Verzweigung übergibt, diese Adresse zeigt auf den nächsten Befehl, der direkt nach dem bl folgt.
====push====
Der Befehl push kopiert die angegeben Register in den Stack. Diese Daten können mit pop wieder geladen werden.
In unserem Beispiel wird die Rücksprungadresse in den Stack abgelegt.
 
<syntaxhighlight lang="asm">
push {lr}
</syntaxhighlight>
 
====pop====
Mit pop werden Daten aus dem Stack in die angegebenen Register geschrieben. In unserem Fall schreiben wir die Rücksprungadresse, die wir zuvor aus '''lr''' gespeichert hatten in den Programmzähler, so dass wir aus der Funktion herausgehen und wieder im Hauptprogramm landen. Dies entspricht einem Return zum aufrufenden Programm.
 
<syntaxhighlight lang="asm">
pop {pc}
</syntaxhighlight>
 
===Erweiterte Funktionen für die LED===
Nun erstellen wir mit diesem Wissen, wie wir Funktionen erstellen, eine Funktion, die selbstständig die entsprechende LED ansteuern kann, ohne dass wir darüber nachdenken müssen, welches GPIOSEL, GPIOSET oder GPIOCLR verwendet werden soll.
Wir werden einfach nur als Übergabewerte, die PIN-Nr. der LED angeben und zwei Funktionen “LED_on” und “LED_off” erstellen. Zusätzlich werden wir noch eine “SetGPIOFunction” erstellen, die uns die Zuweisung der entsprechenden Funktion der Pins übernimmt.
Wir erzeugen eine neue Datei “gpio.h”, in der wir diese Funktionen für GPIO ablegen. Auch hierfür funktioniert die .include Anweisung.
Zusätzlich wird eine Includedatei “time.h” erzeugt, die unsere Wait-Schleife zunächst beherbergt. Allerdings werden wir den Inhalt später nochmal ändern, da es bessere Wege gibt, eine “Wait-Schleife” zu erzeugen.
 
Den Sourcecode gibt es hier: [https://www.satyria.de/arm/source/kurs/2.3.zip 2.3.zip]
 
====Funktion SetGPIOFunction====
Fangen wir von hinten an. Unsere erste Funktion, die wir im Include "gpio.h" erstellen, ist die Zuweisung der Funktion des Registers. Dazu benötigen wir zwei Werte. Zunächst geben wir an welcher Pin angesteuert werden soll und welche Funktion dieser übernehmen soll. Die möglichen Funktionen, die möglich sind, habe ich bereits zuvor erwähnt.
 
<syntaxhighlight lang="asm">
/* void SetGPIOFunction (int Pin(r0), int Function(r1))
*/
 
SetGPIOFunction:
  push {lr}
  ldr r2,=GPFSEL0
</syntaxhighlight>
 
Unser Funktion bekommt den Namen “SetGPIOFunction”. Zunächst legen wir die Rücksprungadresse in den Stapel und weisen '''r2''' das erste GPFSEL Register zu. Noch wissen wir nicht, welche Register das richtige ist. Als nächstes werden wir es berechnen.
 
<syntaxhighlight lang="asm">
SetGPIOFunction_loop1:
  cmp r0,#9
  subhi r0,#10
  addhi r2,#4
  bhi SetGPIOFunction_loop1
</syntaxhighlight>
 
Als wir in die Funktion gesprungen sind, haben wir unseren Pin, den wir ansprechen wollen in r0 übergeben. Wie bereits zuvor beschrieben, kann jedes Register maximal 10 Pin's ansprechen. Damit wir erstmal herausbekommen, welches das richtige Register ist, verwenden wir eine Berechnung.
Wir vergleichen zunächst die PinNr ('''r0''') mit 9 (0-9). Im nächsten Schritt verwenden wir einen kombinierten Befehl “subhi”. Der wird nur ausgeführt, wenn der Inhalt von '''r0''' größer als 9 war. Wenn die Zahl größer war, wird '''r0''' um 10 reduziert, da jedes Register 10 Pins versorgt. Der nächste Befehl ist ähnlich zu sehen, allerdings erhöhen wir die Adresse von GPFSEL um 4. Das ist die nächste GPFSEL Adresse. Dies wird wiederum auch nur dann ausgeführt, wenn das Ergebnis wie bei “subhi” größer als 9 war. Der nächste Befehl “bhi” wird auch nur ausgeführt, wenn die Bedingungen die gleichen waren. Erst wenn die Zahl kleiner oder gleich 9 war, wird der Code weitergeführt, ansonsten wiederholt sich diese ganze Schleife. Am Ende haben wir die richtige GPFSEL-Adresse und den entsprechenden PIN dessen Registers.
Also in '''r2''' steht jetzt die GPFSEL-Adresse und '''r0''' beinhaltet den PIN.
Unser nächstes Problem ist, dass der Wert des PINs nicht mit der Eingabe des Registers übereinstimmt. Jeder PIN hat 3 Bits. Also müssen wir den zuvor Berechnete Wert mit drei multiblizieren. Wir verwenden dafür allerdings keinen Multiplikationsbefehl, da es hierfür eine alternative gibt:
 
<syntaxhighlight lang="asm">
add r0,r0,lsl #1
</syntaxhighlight>
 
Dieser Befehl schiebt den Inhalt (binär) von '''r0''' um eins nach links und addiert dann den Ursprünglichen Wert aus '''r0''' dazu und schreibt es in '''r0''' zurück.
Dies ist eine Möglichkeit, einen Wert mit drei zu multiplizieren.
Beispiel:
 
{| class="wikitable"
|-
| Inhalt von '''r0''': 0010 -> ''Entspricht Dezimal 2''<br>
Verschiebe um 1 nach Links (lsl):<br>
Inhalt: 0100<br>
Addiere nun den alten Wert aus '''r0''':<br>
0100 + 0010 -> 0110 ''Dies entspricht Dezimal 6''
|}
 
Warum verwende ich nicht einen Multiplikation Befehl? Multiplikationen benötigen in der Regel viel Rechenleistung. Solange es über eine Alternative möglich ist und es auch nicht zu einem höheren Aufwand kommt, sollte dies vermieden werden. Da dies in diesem Fall möglich ist, verwenden wir hier diese Methode.
 
Also in '''r0''' steht nun die Richtige Position für das Register. Und unsere Funktion haben wir in '''r1''' bereitgestellt.
 
<syntaxhighlight lang="asm">
lsl r1,r0
</syntaxhighlight>
 
Die Übergabe der Funktion steht in '''r1''' und mit '''lsl''' schieben wir um den Wert aus r0 an die richtige Position.
 
Unser Funktion soll nun nicht andere, eventuell bereits gesetzte Funktionen im verwendeten Register löschen. Dazu werden wir den bereits vorhanden Wert maskieren und erst dann hineinschreiben.
Dazu erzeugen wir erstmal eine Maske.
 
<syntaxhighlight lang="asm">
mov r3,#0b111
lsl r3,r0
mvn r3,r3
</syntaxhighlight>
 
Zunächst setzen wir die Bits (3 Stück), für eine einzelne Funktion ins Register '''r3''' und schieben es an die richtige Position. In '''r0''' steht weiterhin unser berechneter Wert für die Position der entsprechenden Bits. Mit dem Befehl '''mvn''' negieren wir das Ergebnis, sodass wir alle Bits gesetzt haben und nur der Teil, der sich verändert, mit "Nullen" gefüllt ist.
 
<syntaxhighlight lang="asm">
ldr r4,[r2]
and r4,r3
</syntaxhighlight>
Unser Ziel ist es, dass wir alle Funktionen, die eventuell schon entsprechend gesetzt sind, nicht löschen wollen. Dazu laden wir den Inhalt des Registers nach '''r4'''. Nun verwenden wir unser Maske, die in '''r3''' abgelegt ist, mit dem Befehl '''and'''. Dieser Befehl löscht nun genau die Stelle, in der in der Maske "Nullen" stehen. Alle anderen Bits bleiben so, wie sie ursprünglich waren. Siehe auch bei den neuen Befehlen nach, wie genau '''and''' funktioniert.
 
<syntaxhighlight lang="asm">
orr r1,r4
</syntaxhighlight>
 
Mit '''orr''' kompinieren wir nun die Werte aus '''r1''' und '''r4'''. Damit wird die entsprechende Funktion, die wir zuvor Berechnet haben, in die gelöschte Lücke gesetzt. Siehe auch hierzu unter den neuen Befehlen nach, wie '''orr''' funktioniert.
 
Damit stehen in '''r1''' die alten Funktionen und der neue Wert für unser Pin bereit.
 
<syntaxhighlight lang="asm">
str r1,[r2]
</syntaxhighlight>
 
Und wir können den Wert ins Register schreiben.
Damit ist die entsprechende Funktion gesetzt und wir können unsere Funktion wieder verlassen.
 
<syntaxhighlight lang="asm">
pop {pc}
</syntaxhighlight>
 
====Funktion LED_on====
Die Funktion LED_on wird nun etwas leichter sein, als die vorherige. Zunächst legen wir fest, welche Werte wir erwarten. Dies wird einfach nur der PIN sein, der die LED steuert. Also dieser Wert bekommen wir in '''r0''' übergeben.
 
<syntaxhighlight lang="asm">
/* void LED_on (int r0)
  in r0 wird der Pin der LED übergeben
*/
LED_on:
  push {r5,lr}
  mov r5,r0
</syntaxhighlight>
 
Unser Funktion bekommt den Namen LED_on. Zunächst sichern wir das Register '''r5''' und die Rücksprungadresse, die wir in '''lr''' bekommen haben, in den Stapel. '''r5''' müssen wir in diesem Fall sichern, damit wir später den ursprünglichen Wert dort wieder hineinschreiben können. In unserer Funktion verwenden wir '''r5''', damit wir den Wert aus '''r0''' nicht verlieren, da wir eine andere Funktion aufrufen, die eventuell '''r0''' verändern könnte. Dazu habe ich im Unterkapitel zu "Funktionen" bereits beschrieben, warum es wichtig ist, dies zu machen.
 
<syntaxhighlight lang="asm">
  mov r1,#0b001
  bl SetGPIOFunction
</syntaxhighlight>
 
Zunächst setzen wir die "Funktion" als "Ausgabe" des entsprechenden PINs. Dies ist in diesem Fall die Kompination der Bits von "001". Die Funktion "SetGPIOFunction", die wir zuvor geschrieben haben, erwartet als Übergabe in '''r0''' den PIN und in '''r1''' die "Funktion", die auf diesem PIN angewendet wird. In '''r0''' wurde bereits die Position des Pins hinterlegt.
 
<syntaxhighlight lang="asm">
mov r0,r5
</syntaxhighlight>
 
Um es überschaubarer zu machen, wird der Pin, der in '''r5''' steht, wieder nach '''r0''' kopiert. Zuvor haben wir einen Funktionsaufruf durchgeführt und wissen dadurch nicht, was aus unserem '''r0''' wurde. Aus Sicherheitsgründen, auch wenn man eigentlich weiß, dass in '''r0''' noch der alte Wert stehen müsste, sollte man dies tun. Eine Funktion kann sich auch ändern, und eventuell dann das entsprechende Register verändern.
 
Auch bei dieser Funktion wollen wir variabel bleiben, so dass wir nicht wissen, welches GPSET-Register das richtige ist. Dazu berechnen wir auch diesmal das richtige Register.
 
<syntaxhighlight lang="asm">
  ldr r1,=GPSET0
  cmp r0,#31
  subhi r0,#32
  addhi r1,#4
</syntaxhighlight>
 
Zunächst laden wir das erste GPSET-Register nach '''r1'''. Vergleichen '''r0''' (PIN) mit 31. Sollte nun der Wert größer sein subtrahieren wir von diesem Wert 32 und addieren die Adresse des Registers um 4. Diesmal können wir auf eine Schleife verzichten, da es nur zwei Register gibt.
In '''r0''' steht nun die Position des PINs des entsprechenden Registers, welches in '''r1''' steht.
 
<syntaxhighlight lang="asm">
  mov r2,#1
  lsl r2,r0
</syntaxhighlight>
 
Da jede Position der PIN nur einen Bit entspricht, können wir diese Bit, welches wir in '''r2''' abgelegt haben einfach mit der Position ('''r0''') schieben. Damit ist bereits das richtige Bit gesetzt.
 
<syntaxhighlight lang="asm">
  str r2,[r1]
</syntaxhighlight>
 
Damit speichern wir dieses Bit, welches in '''r2''' liegt, nun im Register ('''r1''') ab. Und die LED leuchtet.
 
<syntaxhighlight lang="asm">
  pop {r5,pc}
</syntaxhighlight>
 
Da wir in unserer Funktion '''r5''' verwendet haben, laden wir den ursprünglichen Wert aus dem Stack wieder nach '''r5''' und beenden die Funktion.
 
====Funktion LED_off====
Die Funktion LED_off funktioniert genau so wie die Funktion LED_on. Allerdings haben wir hier die Funktion GPCLR verwendet. Damit wird die LED ausgeschaltet.
 
<syntaxhighlight lang="asm">
  ldr r1,=GPCLR0
</syntaxhighlight>
 
====Funktion wait====
Diese Funktion schreiben wir in eine neue Datei, welche den Namen “time.h” bekommt. Später werden wir diese Datei noch ändern, da es bessere Methoden gibt.
 
<syntaxhighlight lang="asm">
/* void wait (int r0)
  in r0 wird die Anzahl der Durchläufe angegeben
*/
wait:
  push {lr}
wait_begin:
  cmp r0,#0
  beq wait_end
  sub r0,#1
  b wait_begin
wait_end:
  pop {pc}
</syntaxhighlight>
 
====Änderungen am Hauptprogramm====
Im Hauptprogramm können wir nun auf den meisten Code verzichten, da wir nun die Funktionen verwenden können und es wird übersichtlicher.
 
<syntaxhighlight lang="asm">
.include "base.inc"
.include "gpio.h"
.include "time.h"
</syntaxhighlight>
 
Zunächst laden wir unser Zuordnungen und die neuen Funktionen mit ".include" in unseren Code rein. Damit sind diese Funktionen, für unser Hauptprogramm verfügbar, die wir später verwenden.
 
<syntaxhighlight lang="asm">
.section .init
.globl _start
_start:
 
b main
 
.section .text    @Next is code
 
main:            @Label "main"
  mov sp,#0x8000 @Create Stapel (32768 Bytes)
</syntaxhighlight>
 
Als nächste kommt unser obligatorische Start unseres Programms.
Zunächst erzeugen wir unser Label für die Endlosschleife.
 
<syntaxhighlight lang="asm">
MainLoop:
</syntaxhighlight>
 
Die erste Funktion, die wir verwenden, ist "LED_on", damit wir die LED anschalten.
 
<syntaxhighlight lang="asm">
  mov r0,#42
  bl LED_on
</syntaxhighlight>
 
Unsere Funktion, die wir zuvor geschrieben haben, möchte in '''r0''' den entsprechenden GPIO-Pin haben. Unsere LED wird über den Pin 42 angesteuert, diesen Wert schreiben wir in '''r0''' und rufen dann unsere Funktion LED_on auf. Damit leuchtet die LED auf.
 
<syntaxhighlight lang="asm">
  mov r0,#0x3F0000
  bl wait
</syntaxhighlight>
 
Nun warten wir eine Zeitlang mit der Funktion "wait" und anschließend schalten wir die LED aus.
 
<syntaxhighlight lang="asm">
  mov r0,#42
  bl LED_off
</syntaxhighlight>
 
Nun warten wir wieder.
 
<syntaxhighlight lang="asm">
  mov r0,#0x3F0000
  bl wait
</syntaxhighlight>
 
Und springen dann in unsere Endlosschleife.
 
<syntaxhighlight lang="asm">
  b MainLoop
</syntaxhighlight>
 
Damit ist dieses Code für diese Programm schon beendet.
 
===Neue Befehle (2)===
====add (Addieren), addhi====
Hiermit werden zwei Werte addiert. Wie bereits zuvor beschrieben, können alle Befehle "Bedingt" ausgeführt werden.
'''addhi''' ist hier der zusammengesetzte Wert aus "'''add'''" und einem Bedingungscode. In diesem Fall "'''hi'''", was bedeutet, dass die Addition nur durchgeführt wird, wenn zuvor als Ergebnis ein "größer" erfolgte.
 
<syntaxhighlight lang="asm">
addhi r1,#4
</syntaxhighlight>
 
Dieses Beispiel bedeutet, dass das Register '''r1''' nur mit 4 addiert wird, wenn zuvor der Vergleich ein "größer" ergab.
 
<syntaxhighlight lang="asm">
add r0,r0,lsl #1
</syntaxhighlight>
 
Auch sowas ist möglich. Dies ist in diesem Fall eine Kombination aus einer Addition und einem Shift. Hier wird zunächst der Wert aus '''r0''' um eine Position (Binär) nach links verschoben ohne den Wert selbst zu verändern und anschließend das Ergebnis mit '''r0''' addiert.
 
====subhi (subtrahieren)====
'''sub''' hatten wir bereits, allerdings wurde hier eine Bedingung mit angehängt. In diesem Fall wird nur subtrahiert, wenn das Ergebnis größer war.
 
====bhi, beq (Branch mit Bedingung)====
Auch '''b''' hatten wir bereits, auch hier wird mit Bedingung verzweigt. “hi” bei einem Ergebnis größer und “eq” bei Gleichheit.
 
====mvn (move not)====
Damit wird ein Wert in ein Register kopiert und negiert. Das heißt, dass alle Bits des Wertes getauscht werden, Aus “0” wird “1” und umgekehrt.
 
<syntaxhighlight lang="asm">
mvn r3,r3
</syntaxhighlight>
 
Hier wird der Wert aus '''r3''' einfach negiert.
 
====and (logisches UND)====
Hier wird ein Binäres logischen UND ausgeführt.
 
{| class="wikitable"
|-
!  !! BIT !! BIT !! BIT !! BIT
|-
| Wert 1 || 1 || 1 || 0 || 0
|-
| Wert 2 || 1 || 0 || 1 || 0
|-
| Ergebnis || 1 || 0 || 0 || 0
|}
 
Nur wenn in beiden Operatoren ein Bit gesetzt ist, wird dieses im Ergebnis gesetzt. Ansonsten werden diese gelöscht.
 
====orr (logisches ODER)====
Hier wird ein Binäres logisches ODER ausgeführt.
 
{| class="wikitable"
|-
!  !! BIT !! BIT !! BIT !! BIT
|-
| Wert 1 || 1 || 1 || 0 || 0
|-
| Wert 2 || 1 || 0 || 1 || 0
|-
| Ergebnis || 1 || 1 || 1 || 0
|}
 
Sobald ein Bit in einem der Operatoren gesetzt ist, wird dieses im Ergebnis auch gesetzt. Wenn kein Operator ein Bit gesetzt hat, wird dieses gelöscht.
 
-----
 
{| style="width: 100%;
| style="width: 33%;" | [[Das erste Programm|< Zurück (Das erste Programm)]]
| style="width: 33%; text-align:center;" | [[Hauptseite|< Hauptseite >]]
| style="width: 33%; text-align:right;" | [[System Timer|Weiter (System Timer) >]]
|}

Aktuelle Version vom 22. August 2024, 12:01 Uhr

In diesem Kapitel gehe ich auf die GPIO-PINs ein, die der Raspberry Pi für die Kommunikation mit der Außenwelt verwenden kann. Idealerweise ist ein PIN mit einer LED, die auf dem Board verbaut ist, verbunden, so dass wir zunächst auf externe Hardware verzichten können und effektiv ein Ergebnis zu sehen. Unter den Baremetal-Programmieren kommt dies einem “HelloWorld” Programm am nächsten. Im ersten Unterkapitel werden wir die LED blinken lassen, im nächsten versuchen wir etwas Struktur in den Sourcecode zu bekommen und dann den Code soweit verbessern, dass die Funktionen universaler genutzt werden können.

Der Raspberry Pi besitzt einige GPIO, die gerade für Hardware Freunde interessant sind, da diese einfach zu programmieren sind und sich gut mit der Außenwelt verbinden lassen. Auf dem Raspberry Pi 4 gibt es 40 GPIO-Pins, von denen 26 Pins als Ausgang oder Eingang programmiert werden können. Hier mal eine Zusammenfassung der 40 Pins und deren Zuordnung:

Zusätzlich gibt es noch weitere interne GPIO-Adressen, die auch über die Programmierschnittstelle programmiert werden können. Eine davon ist die grüne LED, die über den Port 42 auf dem Raspberry Pi 4 angesteuert werden kann.

LED leuchten lassen

Wie bereits zuvor erwähnt, gibt es auf dem Raspberry Pi 4 eine grüne LED, die wir per Programm ansprechen können und diese an- beziehungsweise abschalten können. Dies wird über den Port 42 gesteuert. Bei den älteren Modellen ist die Adresse eine Andere. Schau bitte im Anhang, welche das sind. Im Kapitel “Erweiterte Funktionen für die LED” ist es sehr einfach, auch dort dann die entsprechende LED anzusteuern, aber vergesse dann nicht, auch die Basis-Adresse des Raspberrys im “base.inc” zu ändern.

Zunächst der komplette Sourcecode:

.equ RPI_BASE, 0xFE000000

.equ GPIO_BASE, RPI_BASE + 0x200000

@ GPIO function select (GFSEL) registers have 3 bits per GPIO
.equ GPFSEL0, GPIO_BASE + 0x0   @GPIO select 0
.equ GPFSEL1, GPIO_BASE + 0x4   @GPIO select 1
.equ GPFSEL2, GPIO_BASE + 0x8   @GPIO select 2
.equ GPFSEL3, GPIO_BASE + 0xC   @GPIO select 3
.equ GPFSEL4, GPIO_BASE + 0x10  @GPIO select 4

@GPIO SET/CLEAR registers have 1 bit per GPIO
.equ GPSET0, GPIO_BASE + 0x1C @set0 (GPIO 0 - 31)
.equ GPSET1, GPIO_BASE + 0x20 @set1 (GPIO 32 - 63)
.equ GPCLR0, GPIO_BASE + 0x28 @clear0 (GPIO 0 - 31)
.equ GPCLR1, GPIO_BASE + 0x2C @clear1 (GPIO 32 - 63)

Dieser Teil des Sourcecodes wird in unsere Ausgangsdatei zwischen den Zeilen 4 und 5 eingefügt. Dieser Teil erzeugt Variablen, die während der Kompilierung für den Assembler verfügbar sind.

/*
* LED is Pin42
* on the GPFSEL4 register Bits 8-6
* 001 = GPIO Pin 42 is an output
*/

   mov r1,#1
   lsl r1,#6  /* -> 001 000 000 */

/*
* 0x10 GPFSEL4 GPIO Function Select 4
*/
 
   ldr r0,=GPFSEL4
   str r1,[r0]

/* 
* Set the 42 Bit
* 25:0 SETn (n=32..57) 0 = No effect; 1 = Set GPIO pin n.
* 42 - 32 = 10
*/

   mov r1,#1
   lsl r1,#10

MainLoop:         @Endlosschleife 

/*
* 0x2C GPCLR1 GPIO Pin Output Clear 1 
*/

   ldr r0,=GPCLR1 @LED ist aus
   str r1,[r0]

/* 
* Waiting
*/

   mov r2,#0x3F0000
wait1$:
   sub r2,#1
   cmp r2,#0
   bne wait1$

/*
* 0x20 GPSET1 GPIO Pin Output Set 1 
*/

   ldr r0,=GPSET1 @LED ist an
   str r1,[r0]

/* 
* Waiting
*/

   mov r2,#0x3F0000
wait2$:
	sub r2,#1
	cmp r2,#0
	bne wait2$

   b MainLoop

Und das eigentliche Programm.

Wenn du jetzt keine Lust hast, das alles abzuschreiben, kannst du den Source auch über zweites.s herunterladen.

Damit du nun auch siehst, was da passiert. Kompiliere es nach dem oben genannten Schema. Ersetze “erstes” durch “zweites” und folge den Anweisungen, wie oben beschrieben. Die beschriebene LED fängt nach einem Boot an zu blinken.

Zunächst beschreibe ich die neuen Befehle, die ich hier verwendet habe und im Anschluss beschreibe ich das Programm, wie es funktioniert.

Neue Befehle

.equ

.equ selbst ist kein Assemblerbefehl für den Prozessor. Dieser gibt dem Assembler selbst eine Anweisung. .equ sagt dem Assembler, dass er eine Variable erzeugen soll mit einem bestimmten Inhalt. Dies ermöglicht dem Programmierer statt für bestimmte Zahlenwerte, einen Variablennamen zu verwenden. Alternativ könnte auch der Befehl .set verwendet werden, der die gleiche Funktion hat. Als Beispiel:

.equ RPI_BASE, 0xFE000000

Hier wird der Variablen “RPI_BASE” die Zahl “0xFE000000” zugewiesen. Aber auch rechnen kann man hier.

.equ GPIO_BASE, RPI_BASE + 0x200000

Dies bedeutet, das der Wert aus “RPI_BASE” mit dem Wert “0x200000” addiert und ergibt dann “0xFE200000”.

Aber was bedeuten diese Werte? Der Raspberry Pi verwendet Adressregister, die bestimmte Funktionen übernehmen. Diese werden an einer bestimmten Adresse im Speicher des Raspberry Pi angeboten, die wir benutzen dürfen. Im Dokument BCM2711 ARM Peripherals werden diese Register beschrieben und wie diese anzuwenden sind. Der erste Wert (RPI_BASE), den wir festlegen, ist die Grundlegende Adresse, der Peripherie, wie sie für den ARM sichtbar ist (Seite 10). Für den Raspberry Pi4 ist das die Adresse 0xFE000000. Auf den älteren Modellen ist es eine andere. Im Anhang “Basisadressen der Modelle” habe ich diese aufgelistet. Wenn nun ein anderes Model verwenden wird, muss genau hier, diese Basisadresse angepasst werden. Um die LED anzusteuern verwenden wir das GPIO-Register. Laut Dokumentation (Seite 83) wäre es “0x215000”. Durch Tests kann ich aber sagen, dass es so nicht stimmen kann. In den Vorgängermodellen war es die Adresse “0x200000” und diese funktioniert auch auf dem Raspberry Pi 4. Leider gibt es in den Dokumenten einige Fehler und im Zweifel hilft hier die Community für den Raspberry Pi. Die Restlichen Werte sind aus dem Kapitel 5.2. des Dokuments herausgeholt und als Variablen definiert.

Register

Kommen wir zu etwas Grundsätzlichen. In einem Prozessor gibt es Register, mit denen ein Prozessor schnell rechnen kann. Auf dem ARM gibt es hierfür 16 Stück, R0-R15 und CPSR. Die Register R13-R15 haben allerdings auch andere Funktionen. R13 wird auch das Stabelregister (SP), R14 Rücksprungadresse (LR) und R15 als Programmzeiger (PC) genannt. In der Regel sollte man diese drei Register auch nur für diese Zwecke verwenden. CPSR ist das Statusregister, welches auch APSR genannt werden kann. Dies enthält eine Kopien der Statusflags der Arithmetic Logic Unit (ALU). Sie werden auch als Bedingungscode-Flags bezeichnet. Der Prozessor verwendet sie, um zu bestimmen, ob bedingte Anweisungen ausgeführt werden sollen oder nicht.

R0 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 R13
(SP)
R14
(LR)
R15
(PC)
CPSR

ldr (Load Register)

Der Befehl ldr holt eine 32-Bit Zahl aus dem Speicher und schreibt sie in ein Register.

ldr r0,=GPFSEL4

In unserem Fall weisen wir r0 die Zahl aus GPFSEL4 (0xFE200010) zu. Allerdings kann der ARM dies nicht direkt, wie es hier steht. Hier benutzen wir eine Funktion des Assemblers, der uns im Speicher diese Zahl ablegt und der Assembler diese Adresse dort einträgt. Vom Grunde her, könnten wir dies genauso tun, in dem wir selbst diese Adresse erstellen und darauf verweisen. In einem späteren Teil des Kurses wird das auch vorkommen.

lsl (Logical Shift Left)

Mit lsl kann man binär Bits verschieben und in diesem Fall werden die Bit nach links verschoben.

Als Beispiel aus unserem Code:

lsl r1,#6

Hier wird der Inhalt aus r1 um 6 Stellen nach links verschoben.

Wenn wir zum Beispiel r1 mit “0x1” belegen, so sieht diese Zahl als Binärcode folgendermaßen aus:

0000 0000 0000 0000 0000 0000 0000 0001

Wenn wir nun wie oben beschrieben um 6 nach links verschieben, wird aus der Zahl folgender Binärcode:

0000 0000 0000 0000 0000 0000 0010 0000

sub (Subtrahieren)

Wie der Befehl schon sagt, subtrahiert man hier etwas.

sub r2,#1

In unserem Beispiel wird der Inhalt von Register r2 mit 1 subtrahiert.

cmp (Compare)

Mit dem Befehl cmp werden Daten verglichen und das Register CPSR mit dem Ergebnis gefüllt. Wir verwenden es bereits im nächsten Befehl.

cmp r2,#0

Hier im Beispiel, wird r2 mit "0" verglichen.

bne (Branch wenn nicht gleich)

Den Befehl b haben wir bereits im vorigen Teil des Kurses erwähnt. b bedeute, dass an eine andere Adresse gesprungen werden soll. Hier kommt nun eine Besonderheit des ARM zum Einsatz. Der ARM kann die meisten seiner Befehle mit Bedingungen ausführen, die über das Statusregister gesteuert werden. Es gibt einige Bedingungen, die im nächsten Abschnitt beschrieben werden.

bne wait1$

In unserem Beispiel wird der Sprung nur durchgeführt, wenn das Ergebnis des vorigen Befehls nicht gleich war. Sollte das Ergebnis gleich gewesen sein, wird der Befehl ignoriert und die nächste Anweisung ausgeführt.

Bedingungscode

Die meisten Befehle des ARMs können mit einem Bedingungscode versehen werden. Möglich sind folgende Suffixe, die einfach an den entsprechenden Befehl angehängt werden:

Suffix Bedeutung
EQ Gleich
NE Nicht gleich
CS oder HS Größer oder gleich (ohne Vorzeichen)
CC oder LO Kleiner (ohne Vorzeichen)
MI Negativ
PL Positiv oder Null
VS Bei Überlauf
VC Kein Überlauf
HI Größer (ohne Vorzeichen)
LS Kleiner oder gleich (ohne Vorzeichen)
GE Größer oder gleich (ohne Vorzeichen)
LT Kleiner (mit Vorzeichen)
GT Größer (mit Vorzeichen)
LE Kleiner oder gleich (mit Vorzeichen)
AL Immer. Wird in der Regel nicht geschrieben

Das Programm (zweites.s)

Unser Programm hat folgenden Ablauf:

  • Raspberry Pi darauf vorbereiten, dass er mit eine LED kommunizieren soll
  • LED Ausschalten
  • Warten
  • LED Anschalten
  • Warten
  • Schleife ausführen

Raspberry Pi vorbereiten

Zunächst müssen wir dem Raspberry mitteilen, was wir vorhaben. Dazu müssen wir dem Register GPFSEL mitteilen, wie er seine Pins verwendet soll. Welche Register hier gemeint sind, habe ich bereits unter Systemprogrammierung / Bare Metal beschrieben. Laut Dokumentation im Kapitel 5 gibt es hierfür fünf GPFSEL Register, die für verschiedene GPIO-Adressen zuständig sind. GPFSEL0 ist für die GPIO-Adressen 0-9, GPFSEL1 für 10-19 usw. zuständig. Da unsere LED über GPIO-Adresse 42 angesprochen wird, müssen wir GPFSEL4 verwenden. Innerhalb des Registers hat jede GPIO-Adresse 3 Bits zur Verfügung um deren Funktion zu definieren oder aber auch abfragen zu können. Diese drei Bits sind wie folgt definiert:

Status des GPIO-Registers
000 PIN ist ein Eingang
001 PIN ist ein Ausgang
100 PIN hat die alternative Funktion 0
101 PIN hat die alternative Funktion 1
110 PIN hat die alternative Funktion 2
111 PIN hat die alternative Funktion 3
011 PIN hat die alternative Funktion 4
010 PIN hat die alternative Funktion 5

Als Status werden nur zwei Zustände fest angegeben. Dies sind die Status, den Pin entweder als Eingang oder als Ausgang zu definieren. Alle anderen Status werden einer alternativen Funktion zugeschrieben. Hierbei bedeutet die alternative Funktion des einen PINs nicht die gleiche Funktion des anderen Bits. Dazu kann in der Dokumentation im Kapitel 5.3 “Alternative Function Assignments” geschaut werden, welche es da gibt. Später, werden wir diese auch verwenden. Da wir nun eine LED ansteuern wollen, müssen wir hier die Funktion “001 - PIN ist ein Ausgang” verwenden. Im Register GPFSEL4 sind die drei Bits 8-6 zuständig für den Pin 42. Also hier müssen wir die Bitfolge “001” eintragen. Eine Möglichkeit wäre, diesen Bitcode einfach direkt reinzuschreiben “0b001000000”, aber da wir etwas in Zukunft blicken und auch etwas mehr Code verstehen wollen, setzen wir einfach r1 auf 1 (entspricht wie 0b001) und schieben es mit dem Befehl lsl, den Inhalt, um 6 Stellen nach links. Das Ergebnis ist das gleiche.

mov r1,#1  @ entspricht 0b001
lsl r1,#6  @ -> 0b001 000 000

Die Funktionsregister sind nichts anderes als Adressen, in denen bestimmte Werte abgelegt sind. Die Peripherie nimmt dort diese Werte heraus und reagiert entsprechend. Um nun unseren Wert an die richtige Stelle zu schreiben, müssen wir zunächst die Adresse des Funktionsregisters in ein Register laden und dann über dieses Register unseren Wert übertragen.

ldr r0,=GPFSEL4 @Lade Adsresse von GPFSEL4 nach r0
str r1,[r0]     @Speichere r1 in diese Adresse

Es ist mit dem ARM-Prozessor nicht möglich, direkt in einen Speicherbereich einen Wert abzulegen. Alle Befehle des ARM-Prozessors besteht aus 32-Bits und können nicht erweitert werden, wie manch anderer Prozessor es könnte. In solch einem 32-Bit-Code steht der Befehl selbst, welche Register verwendet werden, der Bedingungscode usw. übrig bleiben da wenige Bits, die eine Adresse speichern könnten. Kurze Wege wären bei einer Adressierung relativ zum Befehl selbst möglich. Da aber das Register sehr wahrscheinlich außerhalb dieses Bereiches ist, müssen wir einen Umweg gehen. Der Befehl ldr lädt eine Adresse einer Variablen, die zu diesem Befehl Relativ entfernt ist in ein Register. Also hier wird einfach nur ein Wert eingetragen, der besagt, dass die Adresse XYZ um z.B. 200 Bytes weiter abgelegt ist. In unserem Beispiel wird der Inhalt der Adresse der Variablen “GPFSEL4” nach r0 geladen, also der Wert aus GPFSEL4 selbst. Mit dem nächsten Befehl str (store) speichern wir den Inhalt des Registers r1 in die Adresse aus r0. Damit haben wir dem Funktionsregister “GPFSEL4” mitgeteilt, wie es mit unserem Pin umgehen muss.

LED An- und Ausschalten

Um nun unser LED an- beziehungsweise abzuschalten verwenden wir wiederum zwei Register, die uns der Raspberry Pi anbietet. Das sind die Register "GPSET" und "GPCLR". Diese können bestimmte PINs im GPIO setzen oder löschen. Laut Dokumentation auf Seite 89 sind bestimmte Bits für bestimmte PINs zuständig. Schauen wir uns zunächst GPSET an. GPSET0 ist für die PINs 0-31 zuständig und GPSET1 für die PINs 32-57. Also unser PIN, den wir brauchen wird mit GPSET1 angesprochen. Damit wir die Position auch richtig berechnen, schreiben wir hier wieder eine 1 ins Register r1 und schieben den Bit um 10 (42 - 32 = 10) Stellen nach links. Da diese Regel auch für GPCLR1 gilt, benutzen wir später diesen Wert aus dem Register r1 auch dort. Das eigentliche Register "GPSET1" verwenden wir später in unseren Endlosschleife.

mov r1,#1
lsl r1,#10

Die Endlosschleife

Nun kommen wir zu unseren Schleife. Diese bezeichnen wir einfach als "MainLoop" und schreiben ein entsprechendes Label.

MainLoop:

Wie bereits zuvor beschrieben, werden wir das Register GPCLR1 verwenden, um zunächst unsere LED auszuschalten. Dazu laden wir wieder mit ldr die Adresse des Registers für GPCLR1 nach r0 und speichern dann r1 (Unser PIN) in dieses Register.

ldr r0,=GPCLR1 @LED is off
str r1,[r0]

Wenn wir nun die LED jetzt sofort wieder anschalten, so würden wir den Effekt nicht erkennen, da dies so schnell von statten gehen würde. Damit wir es aber erkennen können, müssen wir den Prozessor erstmal etwas beschäftigen. Dazu erstellen wir eine Schleife (wait1$). Dort setzen wir das Register r2 auf einen Wert von 0x3F0000. Innerhalb der Schleife reduzieren wir den Wert um 1 und prüfen, ob r2 gleich Null ist. Solange dies nicht der Fall ist, lassen wir einfach die Schleife nochmal durchlaufen. Sobald es erreicht wurde, läuft das Programm weiter.

   mov r2,#0x3F0000   @Setze r2 auf 3F0000
wait1$:
   sub r2,#1          @Subtrahiere von r2 #1
   cmp r2,#0          @Vergleiche den Wert mit "0"
   bne wait1$         @Wenn es nicht gleich ist, wiederhole die Schleife

Im nächsten Schritt verwenden wir das Register GPSET1 auf die gleiche weise, wie zuvor das GPCLR1 Register. Damit wird die LED eingeschaltet.

ldr r0,=GPSET1 @LED is on
str r1,[r0]

Und wir warten wieder eine Zeitlang mit der Schleife wait2$.

   mov r2,#0x3F0000   @Setze r2 auf 3F0000
wait2$:
   sub r2,#1          @Subtrahiere von r2 #1
   cmp r2,#0          @Vergleiche den Wert mit "0"
   bne wait2$         @Wenn es nicht gleich ist, wiederhole die Schleife

Nun springen wir in unsere Endlosschleife und die LED blinkt.

b MainLoop

Includes verwenden

Wenn wir unseren Code etwas anschauen, werden wir bereits etwas feststellen. Wenn unser Programm etwas mehr können soll, als nur eine LED an und ab zu schalten, werden wir irgendwann zu einem Code kommen, der sehr unübersichtlich wird. Der Compiler bietet uns hierfür die Möglichkeit an, bestimmten Code auszulagern. Unser jetziger Code besteht zur Zeit aus zwei Teilen, einen Deklarationsbereich und dem eigentlichen Code. Um nun zunächst mal das prinzip der "Includes" zu zeigen, werden wir unsere Deklarationen in eine andere Datei auslagern. Am besten ist es auch, diese Datei dann auch so zu gestalten, dass wir immer wieder darauf zugreifen können, egal was wir dann schreiben. Die Deklarationen für den Raspberry Pi bleiben immer gleich und kann dann in anderen Sourcecodes verwendet werden.

Also, das erste Include, welches wir erzeugen, wird eine Declarations-Datenbank werden. Wir nennen sie einfach “base.inc” (Basis-Daten).

/* Raspberry PI 4 Deklarationen
*  Datei: base.inc
*  Erstellt: 17.10.2020
*/

.equ RPI_BASE, 0xFE000000 

.equ GPIO_BASE, RPI_BASE + 0x200000

@ GPIO function select (GFSEL) registers have 3 bits per GPIO
.equ GPFSEL0, GPIO_BASE + 0x0   @GPIO select 0
.equ GPFSEL1, GPIO_BASE + 0x4   @GPIO select 1
.equ GPFSEL2, GPIO_BASE + 0x8   @GPIO select 2
.equ GPFSEL3, GPIO_BASE + 0xC   @GPIO select 3
.equ GPFSEL4, GPIO_BASE + 0x10  @GPIO select 4

@GPIO SET/CLEAR registers have 1 bit per GPIO
.equ GPSET0, GPIO_BASE + 0x1C @set0 (GPIO 0 - 31)
.equ GPSET1, GPIO_BASE + 0x20 @set1 (GPIO 32 - 63)
.equ GPCLR0, GPIO_BASE + 0x28 @clear0 (GPIO 0 - 31)
.equ GPCLR1, GPIO_BASE + 0x2C @clear1 (GPIO 32 - 63)

In unserem Hauptprogramm können wir diese Datei einfach per ".include" hinzufügen. Intern sieht der Assembler diese so an, als wäre das ".include" durch den Code aus dem Include ersetzt. Bei Fehlermeldung gibt uns der Assembler allerdings auch die entsprechende Fehlerzeile aus dem Include wieder, so dass wir relativ schnell sehen, wo den der Fehler ist.

.include "base.inc"

Den Sourcecode gibt es nun als Zip-Datei, da unsere Projekte ab sofort mehrer Dateien enthalten. 2.2.zip

Kompiliert wird das Programm wie zuvor. Es hat sich soweit nichts verändert. Aber wir machen es noch besser und verwendet nun Funktionen.

Funktionen verwenden

Funktionen werden verwendet, wenn man zum Beispiel immer wiederkehrende Dinge erledigen möchte. Aber auch, um verschiedene Dinge aus dem Hauptprogramm herauszulösen, um die Lesbarkeit zu verbessern. Als Beispiel zeige ich hier eine Funktion, die wir so jetzt nicht verwenden werden, aber zur Erklärung durchaus Vorteilhaft ist. Wir erstellen uns nun eine Funktion mit dem Namen “Get_GPIO_Base”.

/*
*   int (r0) Get_GPIO-Base{void}
*   gibt die GPIO Adresse in r0 zurück
*/
Get_GPIO_Base:
   push {lr}
   ldr r0,=GPIO_BASE
   pop {pc}

Da Funktionen in der Regel auch für andere Programme verwendet werden, sollte man jeder Funktion eine Beschreibung beifügen, so dass man später jederzeit nachvollziehen kann, für was den die Funktion verwendet wird. Genauso ist es wichtig, was erwartet die Funktion an Daten und welche bekomme ich zurück, wenn ich sie Aufrufe.

Diese Funktion wird die GPIO-Adresse in r0 zurückgeben und verlangt keine Parameter.

Über ein Label wird die Funktion deklariert. In unserem Fall bekommt die Funktion, den Namen Get_GPIO_Base. Dies ist dann auch der Name, mit der wir später diese Funktion aufrufen können.

Wenn wir eine Funktion aufrufen, wird der Funktion eine Adresse im lr Register übergeben. Dies ist die Adresse, von der die Funktion aufgerufen wurde, plus einem Offset, der den nächsten Befehl angibt. Damit wir später wieder zurückkehren können, müssen wir diese Adresse sichern. Dies erfolgt mit dem Befehl "push". Dieser Befehl schreibt die Adresse in den Stack (siehe auch neue Befehle für Funktionen). Wenn nun unsere Ausführungen der Funktion fertig ist, setzen wir einfach die Adresse mit "pop" in den Programmzähler (pc) und unser Programm, welches diese Funktion aufgerufen hat, wird fortgesetzt.

Unsere Funktion ist nun soweit fertig, so dass wir sie aus unserem Hauptprogramm aufrufen können.

bl Get_GPIO_Base

Mit einem einfachen "b" (branch) wäre es möglich unsere Funktion aufzurufen. Allerdings wird mit einem einfachen "branch" der Zeiger nicht an an lr übergeben. Damit wüsste die Funktion später nicht mehr, von wo den ich diese Funktion aufgerufen habe. Mit dem erweiterten Befehl bl (branch with link) wird diese Adresse ins lr Register geschrieben. In der Funktion wird die Adresse später verwendet, um den nächsten Befehl, der hier folgt, auszuführen.

Wie zuvor berichtet, ist das nur ein Beispiel, hat aber für uns überhaupt keinen Sinn. Dies sollte nur verdeutlichen, wie Funktionen geschrieben werden und funktionieren. In den folgenden Beispielen, verwenden wir diese Art von Funktionen, allerdings gibt es noch einiges zu beachten, welche ich hier nun erläutern werde.

Quasi Standard für Register in Funktionen

Problematisch bei Funktionen ist, dass der Benutzer solcher Funktionen nicht weiß, welche Register in der Funktion verwendet werden. Dies kann zu Komplikationen führen, die so nicht gewollt sind. Man kann sich das gut vorstellen, wenn ich in meinem Programm das Register r5 verwende, um zum Beispiel darin meine Berechnung abzulegen, die ich später weiterhin verwenden will, sollte der Wert auch weiterhin verfügbar sein. Wenn allerdings die Funktion dieses Register verwendet, wird dieses Register überschrieben und mein Wert ist weg. Damit das aber nicht passiert, werden hierfür Regeln aufgestellt, so dass man sich darauf verlassen kann, dass bestimmte Register nicht überschrieben werden oder zumindest nach Rückkehr aus der Funktion, die entsprechenden Register wieder ihren Wert haben.

Assemblerfunktionen haben auch in anderen Hochsprachen ihre Daseinsberechtigung. Auch dort können solche Funktionen aufgerufen werden. Da die Hochsprache hier auch nicht weiß, was den in der Funktion passiert, muss hier auch auf Richtlinien geachtet werden.

Einige Programmierer haben dazu einen Standard entwickelt, an den wir uns hier auch halten werden. Dieser Standard hat den Namen “Application Binary Interface (ABI)”. Dort wurde für den AMD-Prozessor einige Richtlinien festgelegt:

Der Standard besagt, dass r0, r1, r2 und r3 der Reihe nach als Eingaben für eine Funktion verwendet werden können. Wenn eine Funktion keine Eingaben benötigt, spielt es keine Rolle, welche Werte diese beinhalten. Als Rückgabe wird immer das Register r0 verwendet. Wenn eine Funktion keine Rückgabe hat, spielt es keine Rolle, welchen Wert r0 annimmt. Außerdem muss r4 bis r12 nach dem Ausführen einer Funktion dieselben Werte besitzen, wie zuvor beim Start der Funktion. Dies bedeutet, dass man beim Aufrufen einer Funktion sicher sein kann, dass sich der Wert von r4 bis r12 nicht ändert, aber bei r0 bis r3 kann man sich nicht sicher sein.

Wenn man dennoch Register höher als r3 innerhalb der Funktion benötigt, müssen die Werte gesichert werden und am Ende der Funktion wieder hergestellt werden. Um dies zu erfüllen, verwenden wir den Stapel, den wir am Anfang des Hauptprogramms festgelegt haben und verwenden dazu die Befehle push und pop, die uns diese Arbeit abnehmen.

Beispiel:

push {r5,lr}

Dieser Befehl legt die Register lr und r5 im Stapel ab. Mit

pop {r5,pc}

werden die Daten aus dem Stapel wieder geladen. Hierbei ist zu beachten, dass der Zeigers des Stapels auch angepasst wird und die Daten nicht mehr verfügbar sind. Hier im Beispiel wird mit push lr auf den Stabel gelegt und dieser Wert mit pop ins pc Register gelegt.

Neue Befehle die wir für Funktionen verwenden

bl (branch with link)

bl ist ein Branch Befehl, der einen Link mit zur Verzweigung übergibt, diese Adresse zeigt auf den nächsten Befehl, der direkt nach dem bl folgt.

push

Der Befehl push kopiert die angegeben Register in den Stack. Diese Daten können mit pop wieder geladen werden. In unserem Beispiel wird die Rücksprungadresse in den Stack abgelegt.

push {lr}

pop

Mit pop werden Daten aus dem Stack in die angegebenen Register geschrieben. In unserem Fall schreiben wir die Rücksprungadresse, die wir zuvor aus lr gespeichert hatten in den Programmzähler, so dass wir aus der Funktion herausgehen und wieder im Hauptprogramm landen. Dies entspricht einem Return zum aufrufenden Programm.

pop {pc}

Erweiterte Funktionen für die LED

Nun erstellen wir mit diesem Wissen, wie wir Funktionen erstellen, eine Funktion, die selbstständig die entsprechende LED ansteuern kann, ohne dass wir darüber nachdenken müssen, welches GPIOSEL, GPIOSET oder GPIOCLR verwendet werden soll. Wir werden einfach nur als Übergabewerte, die PIN-Nr. der LED angeben und zwei Funktionen “LED_on” und “LED_off” erstellen. Zusätzlich werden wir noch eine “SetGPIOFunction” erstellen, die uns die Zuweisung der entsprechenden Funktion der Pins übernimmt. Wir erzeugen eine neue Datei “gpio.h”, in der wir diese Funktionen für GPIO ablegen. Auch hierfür funktioniert die .include Anweisung. Zusätzlich wird eine Includedatei “time.h” erzeugt, die unsere Wait-Schleife zunächst beherbergt. Allerdings werden wir den Inhalt später nochmal ändern, da es bessere Wege gibt, eine “Wait-Schleife” zu erzeugen.

Den Sourcecode gibt es hier: 2.3.zip

Funktion SetGPIOFunction

Fangen wir von hinten an. Unsere erste Funktion, die wir im Include "gpio.h" erstellen, ist die Zuweisung der Funktion des Registers. Dazu benötigen wir zwei Werte. Zunächst geben wir an welcher Pin angesteuert werden soll und welche Funktion dieser übernehmen soll. Die möglichen Funktionen, die möglich sind, habe ich bereits zuvor erwähnt.

/* void SetGPIOFunction (int Pin(r0), int Function(r1))
*/

SetGPIOFunction:
   push {lr}
   ldr r2,=GPFSEL0

Unser Funktion bekommt den Namen “SetGPIOFunction”. Zunächst legen wir die Rücksprungadresse in den Stapel und weisen r2 das erste GPFSEL Register zu. Noch wissen wir nicht, welche Register das richtige ist. Als nächstes werden wir es berechnen.

SetGPIOFunction_loop1:
   cmp r0,#9
   subhi r0,#10
   addhi r2,#4
   bhi SetGPIOFunction_loop1

Als wir in die Funktion gesprungen sind, haben wir unseren Pin, den wir ansprechen wollen in r0 übergeben. Wie bereits zuvor beschrieben, kann jedes Register maximal 10 Pin's ansprechen. Damit wir erstmal herausbekommen, welches das richtige Register ist, verwenden wir eine Berechnung. Wir vergleichen zunächst die PinNr (r0) mit 9 (0-9). Im nächsten Schritt verwenden wir einen kombinierten Befehl “subhi”. Der wird nur ausgeführt, wenn der Inhalt von r0 größer als 9 war. Wenn die Zahl größer war, wird r0 um 10 reduziert, da jedes Register 10 Pins versorgt. Der nächste Befehl ist ähnlich zu sehen, allerdings erhöhen wir die Adresse von GPFSEL um 4. Das ist die nächste GPFSEL Adresse. Dies wird wiederum auch nur dann ausgeführt, wenn das Ergebnis wie bei “subhi” größer als 9 war. Der nächste Befehl “bhi” wird auch nur ausgeführt, wenn die Bedingungen die gleichen waren. Erst wenn die Zahl kleiner oder gleich 9 war, wird der Code weitergeführt, ansonsten wiederholt sich diese ganze Schleife. Am Ende haben wir die richtige GPFSEL-Adresse und den entsprechenden PIN dessen Registers. Also in r2 steht jetzt die GPFSEL-Adresse und r0 beinhaltet den PIN. Unser nächstes Problem ist, dass der Wert des PINs nicht mit der Eingabe des Registers übereinstimmt. Jeder PIN hat 3 Bits. Also müssen wir den zuvor Berechnete Wert mit drei multiblizieren. Wir verwenden dafür allerdings keinen Multiplikationsbefehl, da es hierfür eine alternative gibt:

add r0,r0,lsl #1

Dieser Befehl schiebt den Inhalt (binär) von r0 um eins nach links und addiert dann den Ursprünglichen Wert aus r0 dazu und schreibt es in r0 zurück. Dies ist eine Möglichkeit, einen Wert mit drei zu multiplizieren. Beispiel:

Inhalt von r0: 0010 -> Entspricht Dezimal 2

Verschiebe um 1 nach Links (lsl):
Inhalt: 0100
Addiere nun den alten Wert aus r0:
0100 + 0010 -> 0110 Dies entspricht Dezimal 6

Warum verwende ich nicht einen Multiplikation Befehl? Multiplikationen benötigen in der Regel viel Rechenleistung. Solange es über eine Alternative möglich ist und es auch nicht zu einem höheren Aufwand kommt, sollte dies vermieden werden. Da dies in diesem Fall möglich ist, verwenden wir hier diese Methode.

Also in r0 steht nun die Richtige Position für das Register. Und unsere Funktion haben wir in r1 bereitgestellt.

lsl r1,r0

Die Übergabe der Funktion steht in r1 und mit lsl schieben wir um den Wert aus r0 an die richtige Position.

Unser Funktion soll nun nicht andere, eventuell bereits gesetzte Funktionen im verwendeten Register löschen. Dazu werden wir den bereits vorhanden Wert maskieren und erst dann hineinschreiben. Dazu erzeugen wir erstmal eine Maske.

mov r3,#0b111
lsl r3,r0
mvn r3,r3

Zunächst setzen wir die Bits (3 Stück), für eine einzelne Funktion ins Register r3 und schieben es an die richtige Position. In r0 steht weiterhin unser berechneter Wert für die Position der entsprechenden Bits. Mit dem Befehl mvn negieren wir das Ergebnis, sodass wir alle Bits gesetzt haben und nur der Teil, der sich verändert, mit "Nullen" gefüllt ist.

ldr r4,[r2]
and r4,r3

Unser Ziel ist es, dass wir alle Funktionen, die eventuell schon entsprechend gesetzt sind, nicht löschen wollen. Dazu laden wir den Inhalt des Registers nach r4. Nun verwenden wir unser Maske, die in r3 abgelegt ist, mit dem Befehl and. Dieser Befehl löscht nun genau die Stelle, in der in der Maske "Nullen" stehen. Alle anderen Bits bleiben so, wie sie ursprünglich waren. Siehe auch bei den neuen Befehlen nach, wie genau and funktioniert.

orr r1,r4

Mit orr kompinieren wir nun die Werte aus r1 und r4. Damit wird die entsprechende Funktion, die wir zuvor Berechnet haben, in die gelöschte Lücke gesetzt. Siehe auch hierzu unter den neuen Befehlen nach, wie orr funktioniert.

Damit stehen in r1 die alten Funktionen und der neue Wert für unser Pin bereit.

str r1,[r2]

Und wir können den Wert ins Register schreiben. Damit ist die entsprechende Funktion gesetzt und wir können unsere Funktion wieder verlassen.

pop {pc}

Funktion LED_on

Die Funktion LED_on wird nun etwas leichter sein, als die vorherige. Zunächst legen wir fest, welche Werte wir erwarten. Dies wird einfach nur der PIN sein, der die LED steuert. Also dieser Wert bekommen wir in r0 übergeben.

/* void LED_on (int r0)
   in r0 wird der Pin der LED übergeben
*/
LED_on:
   push {r5,lr}
   mov r5,r0

Unser Funktion bekommt den Namen LED_on. Zunächst sichern wir das Register r5 und die Rücksprungadresse, die wir in lr bekommen haben, in den Stapel. r5 müssen wir in diesem Fall sichern, damit wir später den ursprünglichen Wert dort wieder hineinschreiben können. In unserer Funktion verwenden wir r5, damit wir den Wert aus r0 nicht verlieren, da wir eine andere Funktion aufrufen, die eventuell r0 verändern könnte. Dazu habe ich im Unterkapitel zu "Funktionen" bereits beschrieben, warum es wichtig ist, dies zu machen.

   mov r1,#0b001
   bl SetGPIOFunction

Zunächst setzen wir die "Funktion" als "Ausgabe" des entsprechenden PINs. Dies ist in diesem Fall die Kompination der Bits von "001". Die Funktion "SetGPIOFunction", die wir zuvor geschrieben haben, erwartet als Übergabe in r0 den PIN und in r1 die "Funktion", die auf diesem PIN angewendet wird. In r0 wurde bereits die Position des Pins hinterlegt.

mov r0,r5

Um es überschaubarer zu machen, wird der Pin, der in r5 steht, wieder nach r0 kopiert. Zuvor haben wir einen Funktionsaufruf durchgeführt und wissen dadurch nicht, was aus unserem r0 wurde. Aus Sicherheitsgründen, auch wenn man eigentlich weiß, dass in r0 noch der alte Wert stehen müsste, sollte man dies tun. Eine Funktion kann sich auch ändern, und eventuell dann das entsprechende Register verändern.

Auch bei dieser Funktion wollen wir variabel bleiben, so dass wir nicht wissen, welches GPSET-Register das richtige ist. Dazu berechnen wir auch diesmal das richtige Register.

   ldr r1,=GPSET0
   cmp r0,#31
   subhi r0,#32
   addhi r1,#4

Zunächst laden wir das erste GPSET-Register nach r1. Vergleichen r0 (PIN) mit 31. Sollte nun der Wert größer sein subtrahieren wir von diesem Wert 32 und addieren die Adresse des Registers um 4. Diesmal können wir auf eine Schleife verzichten, da es nur zwei Register gibt. In r0 steht nun die Position des PINs des entsprechenden Registers, welches in r1 steht.

   mov r2,#1
   lsl r2,r0

Da jede Position der PIN nur einen Bit entspricht, können wir diese Bit, welches wir in r2 abgelegt haben einfach mit der Position (r0) schieben. Damit ist bereits das richtige Bit gesetzt.

   str r2,[r1]

Damit speichern wir dieses Bit, welches in r2 liegt, nun im Register (r1) ab. Und die LED leuchtet.

   pop {r5,pc}

Da wir in unserer Funktion r5 verwendet haben, laden wir den ursprünglichen Wert aus dem Stack wieder nach r5 und beenden die Funktion.

Funktion LED_off

Die Funktion LED_off funktioniert genau so wie die Funktion LED_on. Allerdings haben wir hier die Funktion GPCLR verwendet. Damit wird die LED ausgeschaltet.

   ldr r1,=GPCLR0

Funktion wait

Diese Funktion schreiben wir in eine neue Datei, welche den Namen “time.h” bekommt. Später werden wir diese Datei noch ändern, da es bessere Methoden gibt.

/* void wait (int r0)
   in r0 wird die Anzahl der Durchläufe angegeben
*/
wait:
   push {lr}
wait_begin:
   cmp r0,#0
   beq wait_end
   sub r0,#1
   b wait_begin
wait_end:
   pop {pc}

Änderungen am Hauptprogramm

Im Hauptprogramm können wir nun auf den meisten Code verzichten, da wir nun die Funktionen verwenden können und es wird übersichtlicher.

.include "base.inc"
.include "gpio.h"
.include "time.h"

Zunächst laden wir unser Zuordnungen und die neuen Funktionen mit ".include" in unseren Code rein. Damit sind diese Funktionen, für unser Hauptprogramm verfügbar, die wir später verwenden.

.section .init
.globl _start
_start:

b main

.section .text    @Next is code

main:             @Label "main"
   mov sp,#0x8000 @Create Stapel (32768 Bytes)

Als nächste kommt unser obligatorische Start unseres Programms. Zunächst erzeugen wir unser Label für die Endlosschleife.

MainLoop:

Die erste Funktion, die wir verwenden, ist "LED_on", damit wir die LED anschalten.

   mov r0,#42
   bl LED_on

Unsere Funktion, die wir zuvor geschrieben haben, möchte in r0 den entsprechenden GPIO-Pin haben. Unsere LED wird über den Pin 42 angesteuert, diesen Wert schreiben wir in r0 und rufen dann unsere Funktion LED_on auf. Damit leuchtet die LED auf.

   mov r0,#0x3F0000
   bl wait

Nun warten wir eine Zeitlang mit der Funktion "wait" und anschließend schalten wir die LED aus.

   mov r0,#42
   bl LED_off

Nun warten wir wieder.

   mov r0,#0x3F0000
   bl wait

Und springen dann in unsere Endlosschleife.

   b MainLoop

Damit ist dieses Code für diese Programm schon beendet.

Neue Befehle (2)

add (Addieren), addhi

Hiermit werden zwei Werte addiert. Wie bereits zuvor beschrieben, können alle Befehle "Bedingt" ausgeführt werden. addhi ist hier der zusammengesetzte Wert aus "add" und einem Bedingungscode. In diesem Fall "hi", was bedeutet, dass die Addition nur durchgeführt wird, wenn zuvor als Ergebnis ein "größer" erfolgte.

addhi r1,#4

Dieses Beispiel bedeutet, dass das Register r1 nur mit 4 addiert wird, wenn zuvor der Vergleich ein "größer" ergab.

add r0,r0,lsl #1

Auch sowas ist möglich. Dies ist in diesem Fall eine Kombination aus einer Addition und einem Shift. Hier wird zunächst der Wert aus r0 um eine Position (Binär) nach links verschoben ohne den Wert selbst zu verändern und anschließend das Ergebnis mit r0 addiert.

subhi (subtrahieren)

sub hatten wir bereits, allerdings wurde hier eine Bedingung mit angehängt. In diesem Fall wird nur subtrahiert, wenn das Ergebnis größer war.

bhi, beq (Branch mit Bedingung)

Auch b hatten wir bereits, auch hier wird mit Bedingung verzweigt. “hi” bei einem Ergebnis größer und “eq” bei Gleichheit.

mvn (move not)

Damit wird ein Wert in ein Register kopiert und negiert. Das heißt, dass alle Bits des Wertes getauscht werden, Aus “0” wird “1” und umgekehrt.

mvn r3,r3

Hier wird der Wert aus r3 einfach negiert.

and (logisches UND)

Hier wird ein Binäres logischen UND ausgeführt.

BIT BIT BIT BIT
Wert 1 1 1 0 0
Wert 2 1 0 1 0
Ergebnis 1 0 0 0

Nur wenn in beiden Operatoren ein Bit gesetzt ist, wird dieses im Ergebnis gesetzt. Ansonsten werden diese gelöscht.

orr (logisches ODER)

Hier wird ein Binäres logisches ODER ausgeführt.

BIT BIT BIT BIT
Wert 1 1 1 0 0
Wert 2 1 0 1 0
Ergebnis 1 1 1 0

Sobald ein Bit in einem der Operatoren gesetzt ist, wird dieses im Ergebnis auch gesetzt. Wenn kein Operator ein Bit gesetzt hat, wird dieses gelöscht.


< Zurück (Das erste Programm) < Hauptseite > Weiter (System Timer) >