Programmieren in Maschinensprache auf dem Raspberry Pi

Alle Tutorials die ich bisher gesehen habe, starten mit einem Assembler oder c-Compiler um in Assembler zu programmieren. Ich will hier mal versuchen zu zeigen wie man auch direkt in Maschinensprache programmieren kann, ohne einen Compiler zu benutzen.

Für dieses Tutorial wird die Kenntnis von Hexadezimalzahlen und Binärzahlen vorausgesetzt.

Werkzeuge installieren

Um Maschinensprache-Programme zu erstellen brauchen wir entweder einen Binäreditor oder dann einen Texteditor und Hilfsprogramme um eine Binärdatei in lesbaren Text zu wandeln, und umgekehrt.
Wer direkt einen Binäreditor benutzen will, kann dieses Kapitel überspringen.

Als Editor bevorzuge ich emacs. Es geht aber auch jeder andere Texteditor. Zum Beispiel pico, das auf dem Raspi schon vorinstalliert ist.
Wer den emacs installieren will, kann dieses Kommando eingeben:

sudo apt-get install emacs

Um Hilfsprogramme bequemer zu installieren erstellen wir ein neues Verzeichnis namens "bin". Ausserdem machen wir gleich auch noch ein Verzeichnis um unsere Dateien für dieses Tutorial zu versorgen.
Also im Terminal diese Befehle eingeben:

cd
mkdir bin
mkdir tutorial
ls -a
Damit das Verzeichnis "bin" im aktuellen Pfad sichtbar wird müssen wir den Raspi neu starten. Mit "echo $PATH" bekommen wir eine Liste aller Pfade in denen nach ausführbaren Programmen gesucht wird. Wobei die einzelnen Einträge durch Doppelpunkte getrennt sind. Hier sollte also auch "/home/pi/bin" vorkommen.
Eigentlich ist in ".profile" vorgesehen "bin" in den aktuellen Pfad mit aufzunehmen. Aber bei mir hat das nicht funktioniert, anscheinend wird ".profile" gar nicht aufgerufen, sondern nur ".bashrc".
Wir hängen deshalb noch diese Zeile ans "~/.bashrc" an:
PATH="$HOME/bin:$PATH"
Nach einem Neustart sollte jetzt der Pfad etwa so aussehen:
echo $PATH
/home/pi/bin:/usr/lib/arm-linux-gnueabihf/libfm:/usr/local/sbin:...

Die Hilfsprogramme zum Binärdateien umwandeln hatte ich schon vor Jahren mal geschrieben, und können hier gefunden werden: utili.html
Davon werden aber nur "hdump" und "undump" benötigt. Ich habe deshalb ein neues Archiv erstellt mit nur diesen beiden Programmen. Ausserdem noch undump leicht erweitert.
Hier also das neue Archiv: undump2.tar.gz
So wird es entpackt und installiert:

tar zxvf undump2.tar.gz
cd undump2
make clean
make
make install
Falls make oder gcc nicht gefunden wird, zuerst noch dies machen:
sudo apt-get install build-essential
Die Programme erstellen muss man nicht unbedingt auf dem Raspi. Das sollte auch auf jedem anderen Computer gehen. Obige Hilfsprogramme sind so einfach, dass sie auf jedem Betriebssystem laufen sollten. Und ein Texteditor ist auch auf allen Systemen zu finden.

Erstes Programm erstellen

Unser erstes Programm:
    mov r1, #1  @ Register r1 mit dem Zahlenwert 1 fuellen
L1:
    b L1        @ nach L1 springen (b = branch)

Der MOV-Befehl

Der mov-Befehl ist als Binärzahl so aufgebaut:
1110 0011 1010 0000 0001 0000 0000 0001
Cond   IX XXXS
Ich habe die Binärzahl in Vierergruppen geschrieben, so dass man daraus die Hexadezimalzahl leicht im Kopf berechnen kann:
0xE3A01001
Die ersten 4 Bits (oder erste Ziffer der Hexzahl) ist die Bedingung. E steht für Bedingungslos. Sonst wird nur wenn diese Bedingung erfüllt ist, der Befehl auch wirklich ausgeführt.
Folgende Bedingungen sind möglich:
0 EQ  Gleich Null (Zero-Flag gesetzt)
1 NE  Ungleich Null (Zero-Flag gelöscht)
2 CS  Carry Set   oder >= vorzeichenlos
3 CC  Carry Clear oder < vorzeichenlos
4 MI  Negative Zahl (N-Flag gesetzt)
5 PL  Positiv oder Null (N-Flag gelöscht)
6 VS  Ueberlauf (V-Flag gesetzt)
7 VC  kein Ueberlauf (V-Flag gelöscht)
8 HI  >   vorzeichenlos (nach Vergleich von vorzeichenlosen Zahlen)
9 LS  <=  vorzeichenlos
A GE  >=  (nach Vergleich von vorzeichenbehafteten Zahlen)
B LT  <   (vorzeichenbehaftet)
C GT  >   (vorzeichenbehaftet)
D LE  <=  (vorzeichenbehaftet)
E AL  Immer (always)
Die nächsten 2 Bits sind 0. Danach kommt das Immediate-Bit, welches gesetzt ist wenn direkt eine Zahl ins Register geladen werden soll. In unserem Beispiel also gesetzt. Die folgenden 4 Bits (oben mit XXXX markiert) ist der Befehlscode für diese Befehlsgruppe. 1101 steht für mov.
Hier die Liste der möglichen Befehlscodes:
0xE00... 0000 and  Logisches Und (Operand1 & Operand2)
0xE02... 0001 eor  Logisches Exclusiv Oder
0xE04... 0010 sub  Subtrahieren (Operand1-Operand2)
0xE06... 0011 rsb  Subtrahieren in umgekehrter Reihenfolge (Operand2-Operand1)
0xE08... 0100 add  Addieren (Operand1+Operand2)
0xE0A... 0101 adc  Addieren mit Uebertrag (Operand1+Operand2+Cy)
0xE0C... 0110 sbc  Subtrahieren mit Uebertrag (Operand1-Operand2-!Cy) (Cy ist bei Subtraktionen invertiert!)
0xE0E... 0111 rsc  Subtrahieren in umgekehrter Reihenfolge mit Uebertrag
0xE10... 1000 tst  Logisches Und ohne Resultat speichern, aber Bedingungsflags setzen
0xE12... 1001 teq  Auf 0 testen, dies entspricht einem Exclusiv Oder ohne Resultat speichern
0xE14... 1010 cmp  Vergleichen, Subtrahieren ohne Resultat speichern
0xE16... 1011 cmn  Vergleichen, Addieren ohne Resultat speichern
0xE18... 1100 orr  Logisches Oder
0xE1A... 1101 mov  Wert kopieren, direkte Zahl oder Operand2
0xE1C... 1110 bic  Bits loeschen, entspricht einem Logischen Und mit invertiertem Operand2
0xE1E... 1111 mvn  Invertieren (move not)
Die erste Spalte in dieser Liste ist jeweils der Beginn des Codes wenn ohne Immediate-Bit und S-Bit verwendet. Mit Immediate-Bit ist jeweils die zweite Hexziffer 2 mehr.

Das nächste Bit (S) ist dazu da, um die Bedingungsflags zu setzen. In den Assembler-Abkürzungen (Mnemonics) hängt man jeweils ein s an, wenn dieses Bit gesetzt sein soll. Im Hexcode ist dann jeweils die dritte Ziffer eins mehr.

Die nächsten 4 Bits (die 4. Hexziffer) ist das erste Quellregister (Operand1).
Die nächsten 4 Bits (die 5. Hexziffer) ist das Zielregister (Destination).

Die restlichen 12 Bits (letzte 3 Hexziffern) ist die zweite Quelle (Operand2).
Wenn das Immidiate-Bit gesetzt ist, dann sind diese 12 Bits direkt eine Zahl. Davon werden aber 4 Bits als Schiebewert genommen (Rotation von 2*Schiebewert nach rechts), und nur die untersten 8 Bits können beliebige Zahlen enthalten. Somit sind nur gewisse Zahlen als direkte Zuweisung möglich. Zahlen 0 bis 255 gehen immer, grössere Zahlen nur wenn sie durch rotieren einer 8-Bit-Zahl erzeugt werden können.

Wenn das Immidiate-Bit nicht gesetzt ist, dann sind die nächsten 8 Bits (5.+6. Hexziffer) ein Shift-Wert, und die restlichen 4 Bits ist das zweite Quellregister (Operand2). So können z.B. zwei Register addiert und das Resultat in einem dritten Register gespeichert werden.
Der genaue Aufbau dieser Shift-Werte wird später (beim LSL-Befehl) erklärt.

Da die Register jeweils mit 4 Bits codiert werden, gibt es offensichtlich 16 Register. Die ersten 13 Register, also r0 bis r12 sind allgemein verwendbar. Die anderen drei sind SP (Stack Pointer), LR (Link Register) und PC (Program Counter). Auf SP und LR kommen wir später zu sprechen wenn wir uns mit Unterprogramm-Aufrufen beschäftigen. PC ist der Programmzähler.

Die meisten Befehle der mov-Gruppe benötigen 3 Parameter.
Also z.B. der add-Befehl

  add r1, r2, r3
addiert die beiden Register r2 + r3 und speichert das Ergebnis in r1. Oft wollen wir aber zu einem Register einfach ein zweites dazuaddieren
  add r1, r2
In diesem Fall setzen wir einfach sowohl das Zielregister als auch Operand1 auf r1.
Die Vergleichsbefehle (tst bis cmn) brauchen nur 2 Parameter, das nicht benötige Zielregister wird dann auf 0 gesetzt.
Die Befehle mov und mvn brauchen nur Operand2 und Zielregister. Der nicht benötige Operand1 wird dann auf 0 gesetzt.

Exkurs Programm Counter (PC)

Hier eine kurze Erklärung wie der PC funktioniert:
Nach dem Starten des Computers ist PC auf 0 gesetzt. Das heisst es wird ab der Adresse 0 ein Befehl (der beim ARM fast immer aus einem 32-Bit-Wort besteht) gelesen. Dann wird mit der Ausführung des Befehls begonnen. Gleichzeitig wird PC um 4 erhöht (32-Bit-Wort = 4 Bytes) um auf den nächsten Befehl zu zeigen. Dieser nächste Befehl wird vorsorglich ebenfalls gleich mal geladen (aber noch noch nicht behandelt) und PC wiederum entsprechend erhöht. Nachdem der erste Befehl dann abgehandelt wurde, wird (sofern es kein Sprungbefehl war) der schon vorsorglich geladene Befehl begonnen abzuhandeln, während gleichzeitig wieder ein nächster Befehl vorsorglich eingelesen und PC erhöht wird. Bei einem Sprungbefehl wird der vorsorglich geladene Befehl verworfen um dort weiterzumachen wo der Sprungbefehl hin zeigt.

Der Sprung-Befehl

Der Sprung-Befehl (branch) ist so aufgebaut:
0xEA000000
Dabei ist wieder die erste Ziffer die Bedingung.
Die nächste Ziffer ist der Befehlscode. A steht fuer b (branch), B steht fuer bl (branch linked) was einem Unterprogrammaufruf entspricht.
Die restlichen 6 Ziffern (24 Bits) ist eine Vorzeichenbehaftete Zahl und stellt die Sprungentfernung in 32-Bit Worten dar. Um die Zieladresse zu berechnen wird die Zahl also mit 4 multipliziert und zum PC (Programm Counter) addiert. Wobei man beachten muss dass PC schon auf dem übernächsten Befehl steht. In unserem Beispiel muss also um -2 gesprungen werden (in 32-Bit-Worten), das ist dann als 24-Bitzahl 0xFFFFFE.
Der gesamte Befehl ist also:
0xEAFFFFFE
Da jeweils das niederwertigste Byte zuerst kommt, sieht unser Programm also so aus:
01 10 A0 E3 
FE FF FF EA

Weitere Befehle sind im arm-instructionset.pdf zu finden (einer der ersten Einträge bei entsprechender Google-Suche).

Programm installieren

Damit wir Assembler-Programme ohne Einschränkungen durch das Betriebssystem direkt laufen lassen können, erstellen wir uns eine seperate SD-Karte. Es kann darauf ein beliebiges Linux installiert werden. Wir werden dann das "kernel.img" durch unser Programm ersetzen.

Jetzt erstellen wir eine Datei "kernel.dump" mit diesem Inhalt:

0000: 0110 A0E3 FEFF FFEA 0000 0000 0000 0000
0010:
Aus diesem Hexdump machen wir wieder eine Binärdatei:
undump kernel.dump kernel.img
hdump kernel.img
Die zweite Zeile ist nicht wirklich nötig, nur zum überprüfen ob es geklappt hat.
Jetzt hängen wir den SD-Kartenleser mit der neu erstellten SD-Karte an einen freien USB-Anschluss des Raspi (da wir schon Tastatur und Maus am USB betreiben brauchen wir dazu einen USB-Hub).
Dazu folgende Befehle eingeben:
df -h  ;vor dem Anschliessen der SD-Karte
df -h  ;nach dem Anschliessen
cd /media/unsere-sd-karte/
mv kernel.img kernel_sicherung.img
cd ~/tutorial
cp kernel.img /media/unsere-sd-karte/
unmount /media/unsere-sd-karte/

Jetzt können wir das System runterfahren und mit der neuen SD-Karte wieder starten. Wenn dann nichts passiert, sondern nur die rote Power-LED leuchtet, ist das ok.
Damit unser Programm wirklich was macht müssen wir es noch etwas erweitern.

LED leuchten lassen

Auf dem Raspi gibt es einen Stecker auf den einige GPIO-Leitungen geführt sind. Wir könnten jetzt z.B. am Pin 11 dieses Steckers (GPIO17) eine LED anschliessen (Minus-Pol der LED). Über einen Widerstand (470 Ohm) dann den Plus-Pol der LED mit dem Pin 1 (3.3 Volt) des Steckers verbinden.

Wir können aber auch die schon auf dem Board vorhandene OK-LED verwenden, diese ist über GPIO16 schon angeschlossen.

Um die LED einzuschalten müssen wir ein bestimmtes Bit an einer bestimmten Adresse setzen. Die Adresse des GPIO-Bausteins ist 0x20200000. Bevor wir das Bit zum Einschalten der LED setzen können müssen wir noch den entsprechenden GPIO-Pin als Ausgang definieren. Da verschiedene Funktionen pro Pin möglich sind, werden jeweils 3 Bits für jeden Pin benötigt. Mit 0 wird der Pin als Eingang definiert, mit 1 als Ausgang (Was die Werte von 2 bis 7 bewirken müssen wir jetzt noch nicht wissen). Total hat der GPIO 54 Pins, die von 0 bis 53 durchnummeriert sind. Zum Setzen der Funktion der Pins werden die Offsetaddressen 0, 4, 8, bis 20 verwendet. Jede dieser Adressen ist für 10 Pins verantwortlich. Also an Offset 0 die Pins 0 bis 9, an Offset 4 die Pins 10 bis 19, und so weiter. Um also die Funktion von Pin 16 als Ausgang zu definieren müssen wir an der Adresse 0x20200000+4 die 7. Dreiergruppe von Bits setzen.
Wir sollten also die folgende Zahl an der Adresse 0x20200004 speichern:

0 0100 0000 0000 0000 0000
Wir sollten also sowas machen:
    mov r1, #0x40000
    mov r0, #0x20200004
    str r1, [r0]
Der erste mov-Befehl ist einfach zu kodieren, wieder 0xE3A01001, aber jetzt noch die entsprechende Hex-Ziffer um den Wert zu schieben setzen. Eigentlich sollten wir um 18 Bit nach links schieben, der Shift-Wert im Befehl ist aber ein Rotieren nach rechts. Rotieren bedeutet, dass die Bits die rechts rausfallen von links wieder reingeschoben werden. Also müssen wir um 32-18 = 14 Bits nach rechts rotieren.
Ausserdem wird mit dem doppelten Shift-Wert rotiert. Wir können also nur um geradzahlige Werte rotieren, aber dafür gehen Werte bis 30.
Also zum um 14 Bits zu rotieren müssen wir als Shift-Wert 7 einsetzen. Somit ist der kodierte Befehl so:
0xE3A01701

Der zweite mov-Befehl geht nicht. Diese Zahl lässt sich nicht durch schieben eines einzelnen Bytes erzeugen. Wir können dies aber durch addieren von 3 Werten machen. Nun der dritte Wert, nämlich die Offset-Adresse kann man direkt im str-Befehl (auf den wir gleich noch zu sprechen kommen) machen.
Unser neuer Programmteil sieht dann also so aus:

    mov r1, #0x40000
    mov r0, #0x20000000
    add r0, #0x200000
    str r1, [r0,#4]
Die Kodierung des 2. und 3. Befehls sollte jetzt klar sein. Probieren Sie das als Übung mal selbst zu machen.

Laden/Speichern Befehle (LDR und STR)

Mit LDR wird vom Speicher eingelesen, mit STR in den Speicher geschrieben.
Unser str-Befehl ist so aufgebaut:
0xE5801004
Die erste Hex-Ziffer ist (sie ahnen es schon) wieder die Bedingung.
Die ersten 2 Bits der nächsten Ziffer ist der Befehlscode dieser Gruppe (01).
Dann kommt ein Invertiertes Immediate-Bit für den Offset. In unserem Fall auf 0 gesetzt, da wir direkt die Zahl 4 verwenden wollen.
Wenn das letzte Bit der zweiten Ziffer gesetzt ist, dann wird der Offset vor der Ausführung des Befehls addiert, andernfalls erst danach. In unserem Fall auf 1 gesetzt.

Die dritte Ziffer der Hex-Zahl entspricht folgenden 4 Bits:
U: Wenn gesetzt wird der Offset addiert, sonst subtrahiert.
B: Wenn gesetzt wird nur 1 Byte übertragen, sonst ein 32-Bit-Wort.
W: Write-back, wenn gesetzt, dann wird die Adresse automatisch erhöht und ins Register zurückgeschrieben. In unserem Fall auf 0 gesetzt, da wir das nicht wollen.
L: Load/Store, wenn gesetzt ist es der LDR-Befehl, sonst der STR-Befehl.

Die 4. Ziffer ist das Basis-Register, in unserem Fall also r0.
Die 5. Ziffer ist das Ziel-Register beim LDR-Befehl, das zu speichernde Register beim STR-Befehl.
Die letzten 12 Bits (3 Ziffern) ist der Offset als vorzeichenlose Zahl. In unserem Fall 4.
Wenn das Immediate-Bit nicht gesetzt ist, sind die letzten 4 Bits ein weiteres Register, das den Offset enthält. Mit den andern 8 Bits kann man diesen Wert noch schieben.

LED wirklich einschalten

Bis jetzt haben wir erst den Pin zum die LED ansteuern als Ausgang definiert. Jetzt müssen wir noch das entsprechende Bit setzen um den Ausgang zu aktivieren. Wenn wir die LED so wie oben beschrieben verdrahtet haben (die OK-LED auf dem Board ist in gleicher Weise verdrahtet) dann müssen wir den Ausgang auf 0 setzen damit die LED leuchtet, und auf 1 setzen um sie wieder auszuschalten. Der GPIO hat unterschiedliche Offset-Adressen um die Pins einzuschalten und auszuschalten. Die Offsets 28 und 32 setzen die Pins jeweils auf 1, die Offsets 40 und 44 setzen die Pins auf 0. Dabei werden nur diejenigen Pins geschaltet für die eine 1 gesetzt wurde. Um den Pin 16 auf 0 zu setzen brauchen wir also eine 1 die um 16 Bits nach links geschoben ist. Wir könnten das wieder mit dem mov-Befehl machen, aber wir wollen hier mal einen andern oft gebrauchten Befehl benutzen:
    mov r1, #1
    lsl r1, #16

Schieben nach links (LSL)

Die Abkürzung LSL bedeutet "Logical Shift Left". Entsprechend gibt es auch noch den nach rechts schiebenden Befehl LSR, sowie ASR fuer "Arithmetic Shift Right" was für Vorzeichenbehaftete Zahlen wichtig ist, und noch ROR fuer "Rotate Right".
Diese Befehle sind eigentlich jeweils ein mov-Befehl bei dem das zweite Quellregister (Operand2) identisch mit dem Zielregister ist. Das Immediate-Bit ist nicht gesetzt und somit ist die letzte Hex-Ziffer das zweite Quellregister, und die beiden vorletzten Ziffern bilden den Shift-Wert.
Dieser Shift-Wert ist so aufgebaut:
0000 0000
XXXX XTT0
Oder falls der Wert, um den geschoben wird, ebenfalls in einem Register steht so:
0000 0000
RRRR 0TT1
Dabei sind die mit XXXXX markierten Bits der Schiebewert als vorzeichenlose Zahl, und im zweiten Fall sind die mit RRRR markierten Bits das verwendete Register.
Die mit TT markierten Bits haben dann diese Bedeutung:
00  Links schieben (lsl)
01  Rechts schieben (lsr)
10  Arithmetisch rechts schieben (asr). Also bei Vorzeichenbehafteten Zahlen.
11  Rotieren nach rechts (ror): unterstes Bit wird als oberstes wieder reingeschoben.
In unserem Fall setzen wir das Shift-Feld also so:
1000 0000
Somit sieht unser LSL-Befehl so aus:
0xE1A01801

Gesamtes Programm um LED einzuschalten

Das gesamte Programm sieht also so aus:
0xE3A01701    mov r1, #0x40000
0xE3A00202    mov r0, #0x20000000
0xE2800602    add r0, #0x200000
0xE5801004    str r1, [r0,#4]
0xE3A01001    mov r1, #1
0xE1A01801    lsl r1, #16
0xE5801028    str r1, [r0,#40]
           L1:
0xEAFFFFFE    b L1
Oder als kernel.dump:
0000: 0117 A0E3 0202 A0E3 0206 80E2 0410 80E5
0010: 0110 A0E3 0118 A0E1 2810 80E5 FEFF FFEA
0020:
Mit der neuen Version von "undump" kann man jetzt auch direkt obiges Programm einlesen (also jeweils 4 Byte mit 0x beginnend auf jeder Zeile).

Um gleichzeitig unsere selbst angeschlossene LED auch leuchten zu lassen, brauchen wir nur zwei kleine Änderungen. Zum den Pin17 auch als Ausgang zu schalten, müssen wir im ersten Befehl nur die nächste Dreiergruppe von Bits auch auf 1 setzen. Also statt einer 1 lassen wir eine 9 rotieren. Beim Einschalten der LED müssen wir auch noch das nächste Bit setzen, also im 5. Befehl statt r1 auf 1 setzen wir es auf 3.
Das kernel.dump sieht dann so aus:

0000: 0917 A0E3 0202 A0E3 0206 80E2 0410 80E5
0010: 0310 A0E3 0118 A0E1 2810 80E5 FEFF FFEA
0020:

LEDs blinken lassen

Wir wollen jetzt mal ein Programm erstellen, das die beiden LEDs wechselseitig blinken lässt. (Wenn keine zweite LED angeschlossen ist blinkt dann halt nur die grüne OK-LED.)
Hier das Programm:
0xE3A01709    mov r1, #0x240000    @ GPIO16 und GPIO17 als Ausgang
0xE3A02202    mov r2, #0x20000000
0xE2822602    add r2, #0x200000    @ GPIO-Adresse im r2
0xE5821004    str r1, [r2,#4]
              mov sp, #0x8000      @ Stack Pointer setzen
           L1:
0xE3A01001    mov r1, #1           @ GPIO16 auf 0 setzen, ok-LED ein
0xE1A01801    lsl r1, #16
0xE5821028    str r1, [r2,#40]
0xE3A01001    mov r1, #1           @ GPIO17 auf 1 setzen, zweite LED aus
0xE1A01881    lsl r1, #17
0xE582101C    str r1, [r2,#28]
              ldr r0, [pc,#L5]     @ r0=500000
              bl  pause            @ 0.5 Sekunden lange Pause
0xE3A01001    mov r1, #1           @ GPIO16 auf 1 setzen, ok-LED aus
0xE1A01801    lsl r1, #16
0xE582101C    str r1, [r2,#28]
0xE3A01001    mov r1, #1           @ GPIO17 auf 0 setzen, zweite LED ein
0xE1A01881    lsl r1, #17
0xE5821028    str r1, [r2,#40]
              ldr r0, [pc,#L5]
              bl  pause
0xEAFFFFEE    b L1
           L5:
0x0007A120    .word 500000         @ Konstanter Zahlenwert
           pause:                  @ Aufrufparameter: r0: soviele Microsekunden warten
              push {r4-r8,lr}      @ Register retten
              mov r8, #0x20000000
              add r8, #0x3000
              add r8, #4
              ldm r8, {r4,r5}      @ Timer einlesen, 64-Bit, r5=hoeherwertiger Teil?
              adds r4, r0
              adc r5, #0           @ Endzeitpunkt berechnen
           L2:
              ldm r8, {r6,r7}      @ Timer einlesen, r7=hoeherwertiger Teil?
              subs r6, r4
              sbcs r7, r5          @ Endzeitpunkt vom Timerwert subtrahieren
              bcc L2               @ Springe wenn Endzeitpunkt noch groesser ist (Cy invertiert bei Subtraktion!)
              pop {r4-r8,lr}       @ gerettete Register zurueckholen
              mov pc, lr           @ Ruecksprung
Hier sind die schon bekannten Programmteile bereits codiert.
Mit dem Befehl "mov sp, #0x8000" setzen wir den Stapelspeicher (Stack). Um den Befehl zu kodieren muss man nur noch wissen dass SP dem Register r13 entspricht. Die Adresse 0x8000 ist einigermassen willkürlich. Es muss einfach die Obergrenze eines freien Speicherbereichs sein. Der Stapel wächst von höheren zu tieferen Adressen.
Um den Wert 500000 (oder 0x7A120) ins r0 zu bringen könnten wir wieder den MOV-Befehl und eine Addition verwenden. Wir verwenden hier aber den ldr-Befehl und laden den bei der Markierung "L5:" gespeicherten Wert. Als Basisregister verwenden wir PC was r15 entspricht. Für "#L5" muss noch die Distanz von PC bis nach "L5:" eingetragen werden. Diese Distanz ist wie bei einem Sprungbefehl wieder vom übernächsten Befehl beginnend abzuzählen. Wobei hier Anzahl Bytes zu verwenden sind, also Anzahl Befehle mal 4.
Mit dem Befehl "bl pause" rufen wir ein Unterprogramm auf. Der einzige Unterschied zu einem normalen Sprungbefehl ist, dass die Rücksprungadresse im sogenannten Link-Register (lr) gespeichert wird. Dabei entspricht lr dem Register r14.

PUSH und POP

Das Unterprogramm "pause" beginnt mit einem völlig neuen Befehl "push {r4-r8,lr}". Damit werden die Register an der Stelle wohin sp zeigt zwischengespeichert. Das heisst sp wird zuerst um einen entsprechenden Wert erniedrigt um dann die Register in diesem Speicherbereich zu speichern. In unserem Fall, da wir 6 Register speichern wollen, wird also sp um 24 erniedrigt.
Das Gegenstück ist "pop {r4-r8,lr}", was am Ende des Unterprogramms gemacht wird. Dabei werden die Register vom Speicherbereich auf den sp zeigt wieder gelesen, und danach sp wieder erhöht.

STM und LDM

In Wirklichkeit sind PUSH und POP keine eigenständigen Befehle sondern nur Spezialfälle der Befehle STM und LDM (Store Multiple und Load Multiple).
STM/LDM ist so aufgebaut: (Beispiel mit Bitmuster für obigen PUSH-Befehl)
0xE92D41F0
Die erste Hex-Ziffer ist wieder die Bedingung.
Die nächste Ziffer ist 8 wenn der Offset erst nach dem Speichern oder Laden erfolgen soll, oder 9 wenn der Offset vor dem Speichern oder Laden erfolgen soll. Also im Fall von PUSH muss es 9, im Fall von POP dann 8 sein.

Die dritte Ziffer entspricht folgenden 4 Bits:
U: 0=Offset subtrahieren, 1=Offset addieren. (Also bei PUSH 0, bei POP 1)
S: 1=load PSR or force user mode (ich weiss noch nicht was das heisst)
W: 1=Wert zurück schreiben. Also bei PUSH und POP gesetzt.
L: 1=LDM (Load), 0=STM (Store)

Die 4. Ziffer ist das Basisregister. Bei PUSH und POP also SP was r13 entspricht.
Die restlichen 4 Ziffern (16 Bits) ist die Registerliste. Jedes Bit entspricht einem Register, das niederwertigste r0, dann r1 usw bis r15. Dabei ist r13=SP, r14=LR und r15=PC.

Timer - Microsekunden zählen

Es gibt einen Timer der an der Adresse 0x20003004 in Microsekunden-Schritten hochzählt. Dabei werden 64 Bit verwendet um auch lange Zeiträume zu ermöglichen. In unserem Beispiel wird diese Adresse ins r8 geschrieben (wieder in bekannter Weise mit zwei Additionen).
Mit dem LDM-Befehl, den wir gerade kennengelernt haben, lesen wir dann diese 64 Bit in die Register r4 und r5 ein. Dabei wird zuerst r4 mit den niederwertigeren, dann r5 mit den höherwertigeren Bits geladen.
Mit dem Befehl "adds r4,r0" wird r0 zum r4 dazuaddiert. Bei einem Überlauf wird das Cy-Bit gesetzt. Der anschliessende Befehl "adc" berücksichtigt dann dieses Cy und addiert wenn gesetzt 1 mehr. Mit dieser Befehlsfolge kann man also 64-Bit-Werte addieren. Für die Kodierung schaue man sich den MOV-Befehl an.

Zeitpunkt abwarten

Bei L2 wird wieder der Timer eingelesen, dann wird mit dem berechneten Endzeitpunkt verglichen. Dazu benutzen wir eine 64-Bit-Subtraktion, die ähnlich wie beim Addieren mit der Befehlsfolge "subs" und "sbc" erfolgt (oder "sbcs" da wir die Flags für den bedingten Sprung brauchen). Wenn der Endzeitpunkt grösser ist als der aktuelle Timer-Wert sollte bei der Subtraktion das Cy gesetzt werden. Beim Arm ist es aber so, dass bei Subtraktionen das Cy invertiert wird. Also wenn die subtrahierte Zahl grösser ist, dann wird Cy gelöscht, sonst wird Cy gesetzt. Wir springen also nur wenn die Bedingung "Cy Clear" erfüllt ist, also Befehl "bcc" (branch carry clear).
Man könnte hier auch auf Gleichheit prüfen und mit "bne" (branch not equal) springen. Aber dann könnte das Problem auftreten, dass der Zähler schon eins weiter gezählt hätte und dann würden wir ewig warten.
Wenn also die Zeit erreicht oder überschritten ist, dann wird mit "pop", das wir schon bei "push" besprochen haben, und einem anschliessenden "mov pc,lr" das Unterprogramm beendet. Hier können wir uns noch einen Befehl sparen, indem wir mit dem "pop" statt nach lr gleich nach pc zurückkopieren.
Wir beenden das Unterprogramm also mit diesem Befehl:
0xE8BD81F0    pop {r4-r8,pc}       @ gerettete Register zurueckholen, und Ruecksprung

Weitere Vereinfachungen

Bei der L1-Schlaufe machen wir zwei mal fast das gleiche. Nur die STR-Befehle werden mit unterschiedlichen Offsets verwendet. Um dies zu vereinfachen können wir bei jedem zweiten Durchlauf jeweils die anderen Offsets verwenden. Um dies zu realisieren setzen wir ein Register abwechselnd auf 0 oder 1. Wenn es auf 0 ist, dann setzen wir die einen Offsets, ist es auf 1, dann die andern Offsets.
              mov r3, #0
           L1:
              eors r3, #1          @ zwischen 0 und 1 wechseln und Bedingungsflags entsprechend setzen
0xE3A01001    mov r1, #1
0xE1A01801    lsl r1, #16
0x05821028    streq r1, [r2,#40]   @ Nur machen wenn Bedingungsflag Zero gesetzt ist
0x1582101C    strne r1, [r2,#28]   @ Sonst dies machen
              ...
Der Befehl "eors" ist von der MOV-Gruppe und das angehängte "s" bedeutet, dass wir das S-Bit setzen.
Bei den Befehlen "streq" und "strne" bedeuten die Anhängsel "eq" und "ne" dass wir die entsprechende Bedingung im Befehlscode setzen.

Im Unterprogramm pause hatte ich die Variante mit Abfrage des Timers nicht gleich zum laufen gebracht.
Als Alternative zum Timer können wir auch eine Warteschlaufe machen, in der wir r0 runterzählen und bei Erreichen von 0 abbrechen.

Das gesamte vereinfachte Programm sieht dann folgendermassen aus:

0xE3A01709    mov r1, #0x240000    @ GPIO16 und GPIO17 als Ausgang
0xE3A02202    mov r2, #0x20000000
0xE2822602    add r2, #0x200000    @ GPIO-Adresse im r2
0xE5821004    str r1, [r2,#4]
              mov sp, #0x8000      @ Stack Pointer setzen
              mov r3, #0           @ Register zum zwischen 0 und 1 wechseln
           L1:
              eors r3, #1          @ zwischen 0 und 1 wechseln und Bedingungsflags entsprechend setzen
0xE3A01001    mov r1, #1           @ GPIO16, ok-LED ein oder aus
0xE1A01801    lsl r1, #16
0x05821028    streq r1, [r2,#40]   @ Nur machen wenn Bedingungsflag Zero gesetzt ist, LED ein
0x1582101C    strne r1, [r2,#28]   @ Sonst dies machen, LED aus
0xE3A01001    mov r1, #1           @ GPIO17, zweite LED aus oder ein
0xE1A01881    lsl r1, #17
0x0582101C    streq r1, [r2,#28]   @ Nur machen wenn Bedingungsflag Zero gesetzt ist, LED aus
0x15821028    strne r1, [r2,#40]   @ Sonst dies machen, LED ein
              ldr r0, [pc,#L5]     @ r0=500000
              bl  pause            @ 0.5 Sekunden lange Pause
0xEAFFFFF3    b L1
           L5:
0x0007A120    .word 500000         @ Konstanter Zahlenwert
           pause:                  @ Aufrufparameter: r0: soviele Microsekunden warten
0xE92D41F0    push {r4-r8,lr}      @ Register retten
              lsl r0, #4           @ mit 16 multiplizieren fuer ungefaehr richtige Verzoegerung
           L2:
              subs r0, #1          @ Schlaufe braucht ungefaehr 0.1 Microsekunden
              bne L2               @ Springe wenn Endzeitpunkt noch nicht erreicht ist
0xE8BD81F0    pop {r4-r8,pc}       @ gerettete Register zurueckholen, und Ruecksprung
Wenn wir dieses Programm "blinken.s" genannt haben, und die restlichen Codes noch eingetragen sind, können wir mit der neuen undump-Version unser Programm so installieren:
undump blinken.s kernel.img
cp kernel.img /media/unsere-sd-karte/

Übung macht den Meister

  1. Probieren Sie mal selbst die Kodierung der restlichen Befehl zu machen
  2. Versuchen Sie die Variante mit Abfrage des Timers zum laufen zu bringen
  3. Versuchen Sie das Programm bei gleicher Funktionalität noch weiter zu verkleinern.


Letzte Änderungen

13.Jan.2013: erste Version
16.Jan.2015: einige Tippfehler korrigiert, Variante mit Timer zum Laufen gebracht.
21.Jan.2015: Lösungen zu den Übungen in den nächsten Teil verlagert.
31.Jan.2015: Nachtrag bei LSL: auch noch LSR, ASR und ROR kurz erklärt. Anmerkungen bei mov-Gruppe über Anzahl Parameter angefügt.
10.Feb.2015: Nachtrag in der Tabelle der Bedingungen: Hinweise auf Zero- N- und V-Flag

Fortsetzung: Teil2

Kontakt-Formular

Hier das Kontakt-Formular für Fragen und Bemerkungen zum Tutorial.
Last update: 10.Feb.2015 / Rolf                                                                                 Validator