Programmieren in Maschinensprache auf dem Raspberry Pi - Teil2

Dies ist die Fortsetzung dieses Tutorials: Teil1

Lösungen zu den Übungen vom Teil1

Bitte versuchen Sie zuerst die Übungen vom letzten Teil selbst zu machen bevor Sie hier weiterlesen.

  1. Vollständig codierte vereinfachte Variante: blinken.s
  2. Meine Lösung mit Timer-Variante: blinken2.s , Die Schwierigkeit war, dass beim Arm bei Subtraktionen das Carry jeweils invertiert ist.
  3. Ich habe noch 3 kleine Optimierungsmöglichkeiten gefunden:
    Erstens statt die Timer-Adresse mit einem mov- und 2 add-Befehlen zu setzen, die Adresse als Konstante gespeichert und mit ldr geladen. (Achtung, nicht vergessen die Sprungdistanz bei "bl pause" anzupassen!)
    Zweitens in der L1-Schlaufe statt r1 auf 1 zu setzen und mit lsl zu schieben, direkt den mov-Befehl verwendet. Die Schreibweise #(1<<16) macht deutlich was hier gemacht wurde, nämlich die Zahl 1 um 16 Bits nach links geschoben.
    Drittens für den GPIO17 erneutes setzen von r1 weggelassen und mit dem lsl-Befehl das schon gesetzte r1 noch um 1 weiter geschoben. Und Sprungdistanz bei "b L1" angepasst.
    Zusammen haben wir mit diesen Optimierungen 12 Bytes gespart. blinken2optimiert.s

Werkzeuge installieren - auch auf anderem Computer

Im ersten Teil habe ich beschrieben wie man die Werkzeuge auf dem Raspi selbst installiert. Für dieses Tutorial ist es aber bequemer die Programme auf einem andern Computer zu erstellen. Dieser andere Computer kann ein zweiter Raspi sein, oder ein anderer Linux-Computer. Auf MacOSX (mit Version 10.7.5 getestet) geht es aber genau so gut.
Auf einem Windowsrechner sollte es auch funktionieren, sofern Sie es schaffen undump2 zu compilieren.
Auf Linux-ähnlichen Computern funktioniert die Werkzeuge-Installation gleich wie auf dem Raspi.
Die Installation auf einem Mac geht ähnlich und wird im folgenden Abschnitt noch beschrieben.

Werkzeuge auf MacOSX installieren

Um eine neue SD-Karte (mit 8GB für Kameras getestet) vorzubereiten, genügt es diese zu unmounten, dann ein beliebiges System zu installieren, und kernel.img jeweils durch unser Programm zu ersetzen.
Um die Karte zu unmounten, wird in einem Terminal-Fenster mit dem Befehl "df -h" vor und nach dem Einstecken geprüft wie die Disk heisst. (z.B. /dev/disk2s1)
Mit folgendem Befehl wird dann ausgehängt, aber die Karte nicht entnommen:
sudo diskutil unmount /dev/disk2s1
Dann mit folgendem Befehl das Diskimage gespeichert:
sudo dd if=./2012-12-16-wheezy-raspian of=/dev/disk2 bs=1m
Vorsicht dass wirklich die richtige Disk angegeben wird!
Dies dauert einige Minuten.
Danach wird die SD-Karte jeweils als /Volumes/boot erkannt.
Dann das aktuelle Programm mit "cp kernel.img /Volumes/boot/" kopieren, dann SD-Karte auswerfen und in den Raspi einstecken.
Ohne Bildschirm, Tastatur oder Maus (die für dieses Tutorial noch nicht gebraucht werden) kann dann der Raspi direkt an einem USB-Port des Mac angeschlossen werden.

Meine selbst geschriebenen Utilities werden gleich wie unter Linux installiert.
Also z.B. das undump2.tar.gz wird so installiert:

mkdir ~/bin  ;falls nicht schon gemacht, dann Terminal neu starten
tar zxvf undump2.tar.gz
cd undump2/
make clean
make
make install

Neue Werkzeuge installieren

Wenn Sie die Übungen von Teil1 gemacht haben, haben Sie sicherlich gemerkt, dass es ziemlich schwierig ist bei der Codierung der Befehle keine Fehler zu machen. Und wenn man Fehler gemacht hat, dann ist es recht schwierig diese zu lokalisieren.
Ich habe deshalb (und auch um wieder mal etwas c++ zu üben) versucht einen einfachen Disassembler zu programmieren. Dieser kann dann dazu verwendet werden die Codierung zu überprüfen. Es werden noch nicht alle Befehle erkannt, und vielleicht habe ich auch die Syntax nicht ganz korrekt programmiert. Aber mit den bisher besprochenen Befehlen sollte es schon mal korrekt funktionieren.
Hier das Programm: armdisassembler.tar.gz

Zum Installieren wieder wie üblich die folgenden Schritte machen:

tar zxvf armdisassembler.tar.gz
cd armdisassembler/
make clean
make
make install
Dann das Programm z.B. zum Überprüfen von blinken2.s anwenden:
cd ~/tutorial/
undump blinken2.s kernel.img
da kernel.img >test.s
Jetzt sollten Sie test.s mit blinken2.s vergleichen.
Wenn ">test.s" weggelassen wird zeigt der Disassembler das Resultat direkt im Terminal an.

Timer Vergleichsregister

Bisher haben wir vom Systemtimer nur den Zähler verwendet. Ausser dem Zähler gibt es aber im Timer noch einige andere Register. Der Adressbereich des Timers ist 0x20003000 bis 0x2000301B.
Bei der Beschreibung der Register werden im Datenblatt dann jeweils die Offsets von der Basisadresse als Hexadezimalzahlen angegeben:
 0   CS (Control-Status)
 4   Zähler (untere 32 Bits)
 8   Zähler (obere 32 Bits)
 C   Compare 0
10   Compare 1
14   Compare 2
18   Compare 3
Da der Zähler bei 1MHz über eine Stunde braucht um die unteren 32 Bits durchzuzählen, reicht es völlig aus dass die Vergleichsregister auf 32 Bit beschränkt sind.
Die untersten 4 Bits im CS entsprechen dem Status der Vergleichsregister. Im Handbuch sind diese Bits mit M0,M1,M2,M3 bezeichnet. Diese Bits können jederzeit gelesen werden und zeigen an ob das entsprechende Vergleichsregister erreicht wurde. Also wenn der Zähler Compare 0 erreicht hat wird M0 gesetzt, wenn er Compare 1 erreicht hat wird M1 gesetzt, usw. Diese Bits bleiben dann gesetzt bis wir sie wieder rücksetzen. Es mag etwas verwirren, dass wir eine 1 hineinschreiben müssen um ein Bit rückzusetzen. Aber beim Hineinschreiben schreiben wir nicht wirklich ins CS-Register, sondern sagen der Timer-Hardware, dass sie bitteschön die Bits welche wir als 1 angegeben haben im CS rücksetzen soll, alle andern Bits sollen unverändert bleiben.

Jetzt können wir unser Pause-Unterprogramm etwas eleganter programmieren:

           Timeradr:
0x20003000    .word 0x20003000    @ Timer-Basis-Adresse als konstanter Zahlenwert
           pause:                 @ Aufrufparameter: r0: soviele Microsekunden warten
0xE92D4130    push {r4,r5,r8,lr}  @ Register retten
0xE51F8010    ldr r8, Timeradr    @ Timerbasisadresse laden
0xE5984004    ldr r4, [r8,#4]     @ Zaehler des Timers einlesen, nur untere 32-Bit
0xE0844000    add r4, r0          @ Endzeitpunkt untere 32 Bits berechnen
0xE588400C    str r4, [r8,#12]    @ speichern im Vergleichsregister "Compare 0"
0xE3A05001    mov r5, #1          @ Zum das Bit rueckzusetzen wirklich eine 1 schreiben!
0xE5885000    str r5, [r8,#0]     @ Bit M0 im CS ruecksetzen
           L3:
0xE5985000    ldr r5, [r8,#0]     @ neuer Status abfragen
0xE2155001    tst r5, #1          @ ist M0 gesetzt?
0x0AFFFFFC    beq L3              @ nein-> warten bis gesetzt
0xE8BD8130    pop {r4,r5,r8,pc}   @ gerettete Register zurueckholen, und Ruecksprung
Ich habe mal einen kleinen Fehler eingebaut. Mit obigem Disassembler sollten Sie den leicht finden.
Falls dieser Fehler nicht korrigiert wird, läuft das Programm trotzdem korrekt. Aber wundern Sie sich nicht, wenn dann plötzlich in einer erweiterten Version etwas schief läuft.

Wenn Sie sich über den Befehl "ldr r8, Timeradr" wundern, das ist lediglich eine abgekürzte Schreibweise für "ldr r8, [pc,#Timeradr]".

Ein weiterer Befehl, den wir bisher noch nicht verwendet haben, ist noch "tst".
Dieser Befehl gehört zur Mov-Gruppe und wird hier verwendet um den Zustand eines einzelnen Bits zu testen. Wenn das Bit 0 ist wird das Zero-Flag gesetzt, sonst nicht. Genau genommen ist es "tsts", aber da der einzige Sinn dieses Befehls ist, die Flags zu setzen, lässt man das 's' weg und setzt das S-Flag trotzdem.
Gleiches gilt auch für die Befehle teq cmp cmn. Auch hier setzt man das S-Flag immer.

Alle Vergleichsregister verwenden

Wenn Sie jetzt denken, so viel eleganter ist das Programm bei Verwendung des Vergleichsregisters nun auch wieder nicht, dann versuchen Sie doch mal ohne Vergleichsregister ein Programm zu schreiben bei dem 4 LEDs unabhängig voneinander mit verschiedenenen Frequenzen blinken.

Genau dieses Beispielprogramm wollen wir jetzt unter Verwendung aller Vergleichsregister entwerfen.
Dazu sollten Sie (falls nicht schon gemacht) mindestens 4 LEDs am GPIO anschliessen. Zum Beispiel gemäss diesem Schema: ../index.html#gpio

Wir machen also einen ersten Entwurf unseres Programms:

@ blinken4.s  lasse 4 LEDs unabhaengig voneinander blinken
0xE3A0D902    mov sp, #0x8000      @ Stack Pointer setzen
              bl gpio_initialisieren
              ldr r0, zeit1
              ldr r1, zeit2
              ldr r2, zeit3
              ldr r3, zeit4
              mov r6, 0xF   @ 4 unterste Bits im r6 sollen den Status der LEDs representieren
          L1:
              bl leds_gemaess_r6_schalten
              bl pause
              b L1
      zeit1: .word 500000  @ 0.5 Sekunden, also 1 Hz Blinkfrequenz
      zeit2: .word 600000  @ 0.6 Sec, also etwas langsamer blinken, etwa 0.833 Hz
      zeit3: .word 700000  @ 0.7 Sec, etwa 0.714 Hz
      zeit4: .word 800000  @ 0.8 Sec, genau 0.625 Hz

           pause:                  @ Verschieden lange Zeiten warten
0xE92D403F     push {r0-r5,lr}     @ Register retten
               ldr r8, Timeradr    @ Timerbasisadresse laden
           L3:
0xE5985000     ldr r5, [r8,#0]     @ neuer Status abfragen
               ands r5, #0xF       @ ist mindestens eins der Bits M0,M1,M2,M3 gesetzt?
0x0AFFFFFC     beq L3              @ nein-> warten bis gesetzt
0xE5984004     ldr r4, [r8,#4]     @ Zaehler des Timers einlesen, nur untere 32-Bit
0xE0800004     add r0, r4          @ 1. Endzeitpunkt untere 32 Bits berechnen
0xE588000C     str r0, [r8,#0xC]   @ speichern im Vergleichsregister "Compare 0"
               add r1, r4          @ 2. Endzeitpunkt berechnen
               str r1, [r8,#0x10]  @ speichern im Vergleichsregister "Compare 1"
               add r2, r4          @ 3. Endzeitpunkt berechnen
               str r2, [r8,#0x14]  @ speichern im Vergleichsregister "Compare 2"
               add r3, r4          @ 4. Endzeitpunkt berechnen
               str r3, [r8,#0x18]  @ speichern im Vergleichsregister "Compare 3"
0xE3A0500F     mov r5, #0xF        @ Alle 4 Bits im CS rueckzusetzen
0xE5885000     str r5, [r8,#0]     @ Bit M0-M3 alle ruecksetzen
               eor r6, r5          @ r6 aktualisieren
0xE8BD803F     pop {r0-r5,pc}      @ gerettete Register zurueckholen, und Ruecksprung
Jetzt fehlen aber noch einige Teile. Ausser den noch fehlenden Unterprogrammen, müssen wir in "pause" noch jeweils nur die Endzeitpunkte neu setzen, welche auch abgelaufen sind. Ausserdem müssen die Endzeitpunkte vor Aufruf von "pause" noch ein erstes mal gesetzt werden.

Um wirklich nur die abgelaufenen Vergleichsregister neu zu setzen können wir sowas machen:

  tst r5, #1           @ Bit M0 gesetzt?
  addne r0, r4         @ ja: neue Zeit berechnen
  strne r0, [r8,#0xC]  @     und im Compare 0 speichern
Entsprechendes machen wir dann auch für die andern Bits, also "tst r5, #2" fuer M1, usw.
In der Warteschlaufe habe ich "ands r5,#0xF" verwendet. Man könnte da auch "tst r5,#0xF" machen. Der einzige Unterschied ist, dass bei "ands" das Ergebnis wieder in r5 gespeichert wird. Bei "tst" wird das Ergebnis verworfen, nur die gesetzten Bedingungsflags sind dann relevant.
Wenn im CS wirklich immer nur eins (oder mehrere) der untersten 4 Bits gesetzt sind, dann macht es keinen Unterschied ob wir "ands" oder "tst" benutzen. Wenn aber vielleicht in einer erweiterten Hardware im CS auch noch andere Bits gesetzt werden, dann sollten wir diese im r5 wieder löschen. Genau das machen wir bei Verwendung von "ands". Somit sind wir sicher, dass wir keine Bits verwenden von denen wir nicht wissen was sie tun.

Um das Unterprogramm leds_gemaess_r6_schalten zu schreiben wäre ein Unterprogramm hilfreich, wo wir die GPIO-Nummer direkt als Zahl angeben können, und ein Register das sagt ob der Ausgang ein- oder aus-geschaltet werden soll.
Hier ein Vorschlag für dieses Unterprogramm:

           gpio_ein_aus:          @ Parameter: r0=Nummer, r1.bit0=ein/aus (0=aus, 1=ein), r7=GPIO-Basisadresse
0xE92D4004    push {r2,lr}        @ Register retten
0xE3A02001    mov r2, #1
0xE1A02012    lsl r2, r0          @ r2 = (1<<r0)
0xE3111001    tst r1, #1          @ ist erstes Bit in r1 gleich Null?
0x05872028    streq r2, [r7,#40]  @ ja: Spannung aus
0x1587201C    strne r2, [r7,#28]  @ nein: Spannung ein
0xE8BD8004    pop {r2,pc}         @ gerettete Register zurueckholen, und Ruecksprung
Vielleicht sollten wir die Initialisierung des GPIO auch gleich als Unterprogramm schreiben. Wir werden das wohl noch öfter brauchen.
           gpio_initialisieren:    @ Rueckgabeparameter: r7=GPIO-Basisadresse
0xE92D4001    push {r0,lr}        @ Register retten
0xE3A07202    mov r7, #0x20000000
0xE2877602    add r7, #0x200000    @ GPIO-Adresse im r7
@ alle an LEDs angeschlossenen GPIO-Pins als Ausgang setzen:
0xE3A00302    mov r0, #(1<<9*3)    @ GPIO09 blaue LED
0xE5870000    str r0, [r7,#0]
0xE3A00701    mov r0, #(1<<6*3)    @ GPIO16 gruen OK-LED
0xE2800602    add r0, #(1<<7*3)    @ GPIO17 rote LED
0xE2800001    add r0, #1           @ GPIO10 gruene LED
0xE5870004    str r0, [r7,#4]
0xE3A00602    mov r0, #(1<<7*3)    @ GPIO27 gelbe LED
0xE2800D01    add r0, #(1<<2*3)    @ GPIO22 orange LED
0xE5870008    str r0, [r7,#8]
0xE8BD8001    pop {r0,pc}          @ gerettete Register zurueckholen, und Ruecksprung

Um die Timer-Vergleichsregister zum ersten mal setzen, hatte ich wie oben angedeutet mal ein weiteres Unterprogramm "timer_setzen" entworfen. Aber da dieses wieder fast das gleiche macht wie "pause" habe ich eine elegantere Lösung gefunden:

               mov r5, #0xF        @ r5 setzen fuer ersten Durchlauf von pause
           L1:
               bl leds_gemaess_r6_schalten
               bl pause
               b L1
           pause:                  @ Verschieden lange Zeiten warten
0xE92D411F     push {r0-r4,r8,lr}  @ Register retten
               ldr r8, Timeradr    @ Timerbasisadresse laden
               tst r5, #0xF        @ ist r5 schon gesetzt?
               bne L3              @ ja-> Warteschlaufe ueberspringen
           L2:
0xE5985000     ldr r5, [r8,#0]     @ neuer Status abfragen
               ands r5, #0xF       @ ist mindestens eins der Bits M0,M1,M2,M3 gesetzt?
0x0AFFFFFC     beq L2              @ nein-> warten bis gesetzt
           L3:

               eor r6, r5          @ r6 aktualisieren
               mov r5, #0          @ r5 loeschen fuer naechsten Durchlauf
0xE8BD811F     pop {r0-r4,r8,pc}   @ gerettete Register zurueckholen, und Ruecksprung
Beim ersten Durchlauf wird also pause mit gesetztem r5 aufgerufen, danach ist r5 jeweils 0.
Jetzt sollten eigentlich alle wesentlichen Teile vorhanden oder beschrieben sein um das Programm zu vervollständigen.

Übungen

  1. Versuchen Sie das Programm blinken4.s zu vervollständigen.
  2. Versuchen Sie ob das Laden von zeit1 bis zeit4 eleganter zu machen, indem Sie nur ein Label setzen.
  3. Versuchen Sie eine Erklärung zu finden warum Übung 2 nicht funktioniert.


Letzte Änderungen

25.Jan.2015: Erstellung
31.Jan.2015: Etwas genauere Erklärungen beim Timer angefügt, fehlende Kommentare bei Programmteilen angefügt

Fortsetzung: Teil3

Kontakt-Formular

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