Für dieses Tutorial wird die Kenntnis von Hexadezimalzahlen und Binärzahlen vorausgesetzt.
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 -aDamit 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.
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 installFalls make oder gcc nicht gefunden wird, zuerst noch dies machen:
sudo apt-get install build-essentialDie 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.
mov r1, #1 @ Register r1 mit dem Zahlenwert 1 fuellen L1: b L1 @ nach L1 springen (b = branch)
1110 0011 1010 0000 0001 0000 0000 0001 Cond IX XXXSIch habe die Binärzahl in Vierergruppen geschrieben, so dass man daraus die Hexadezimalzahl leicht im Kopf berechnen kann:
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.
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, r3addiert die beiden Register r2 + r3 und speichert das Ergebnis in r1. Oft wollen wir aber zu einem Register einfach ein zweites dazuaddieren
add r1, r2In diesem Fall setzen wir einfach sowohl das Zielregister als auch Operand1 auf r1.
0xEAFFFFFEDa 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).
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.imgDie zweite Zeile ist nicht wirklich nötig, nur zum überprüfen ob es geklappt hat.
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.
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 0000Wir 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.
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.
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.
mov r1, #1 lsl r1, #16
0000 0000 XXXX XTT0Oder falls der Wert, um den geschoben wird, ebenfalls in einem Register steht so:
0000 0000 RRRR 0TT1Dabei sind die mit XXXXX markierten Bits der Schiebewert als vorzeichenlose Zahl, und im zweiten Fall sind die mit RRRR markierten Bits das verwendete Register.
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:
0xE1A01801
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 L1Oder 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:
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 @ RuecksprungHier sind die schon bekannten Programmteile bereits codiert.
0xE92D41F0Die erste Hex-Ziffer ist wieder die Bedingung.
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.
0xE8BD81F0 pop {r4-r8,pc} @ gerettete Register zurueckholen, und Ruecksprung
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.
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 RuecksprungWenn 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/