Programmieren in Maschinensprache auf dem Raspberry Pi - Teil3

Dies ist die Fortsetzung dieses Tutorials: Teil1 , Teil2

Lösungen zu den Übungen vom Teil2

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

  1. Meine bisherige Lösung: blinken4.s , es gibt aber noch einen seltsamen Fehler. Die erste LED blinkt aus mir noch unbekannten Gründen nicht. Noch seltsamer: wenn ich eine Verzögerung zwischen Status lesen und neue Zeiten setzen einbaue, dann geht die erste, aber die dritte LED blinkt nicht korrekt.
  2. Die Idee war r4 auf die Adresse der ersten Zeit zu setzen und dann 4 mal den ldr-Befehl mit Postincrement zu benutzen.
  3. Der Grund dass es nicht funktionierte: die Startadresse unseres Programms ist nicht 0 sondern 0x8000. Mehr dazu in einem der nächsten Abschnitte.

Crosscompiler installieren

Inzwischen sind unsere Programme schon so gross geworden, dass die Codierung von Hand ziemlich aufwändig und Fehleranfällig ist.
Wir könnten uns jetzt ein Programm schreiben, das die Codierung automatisch macht. Mit den bisherigen Befehlen währe das nicht besonders schwierig. Aber das hat schon jemand gemacht, sogar mit Unterstützung von noch vielen weiteren Befehlen. Wir brauchen also nur das entsprechende Programm zu installieren. Eigentlich spricht man von so einem Programm von einem "Assembler", in Wirklichkeit ist es jedoch ein Compiler.
In unserem Fall heisst der Compiler "gcc". Darin ist ausser dem "Assembler" auch ein Compiler für c und c++ enthalten.

Crosscompiler auf Linux installieren

Es sollte das Paket gcc-arm-linux-gnueabi oder arm-none-eabi-gcc oder ähnlich installiert werden. Welches wirklich das ideale ist weiss ich auch noch nicht. Bei mir funktioniert bisher das gcc-arm-linux-gnueabi einwandfrei.
Also z.B. so installieren:
apt-get install gcc-arm-linux-gnueabi

Crosscompiler auf MacOSX installieren

... noch zu machen ...
 port install arm-none-eabi-gcc
 port install arm-none-eabi-binutils

Compiler direkt auf dem Raspberry

Um direkt auf dem Raspberry zu compilieren, brauchen wir keinen Crosscompiler, sondern können direkt den gcc des Systems benutzen.

Assembler-Programm compilieren

Wir wollen mal eins unserer Beispiele mit dem Crosscompiler (oder direkt auf Raspi mit normalem Compiler) übersetzen.
Dazu erst mal einen neuen Ordner erstellen und ein Beispiel kopieren:
cd tutorial
mkdir teil3
cd teil3
cp ../blinken4.s test1.s
Dann müssen wir noch die bereits von Hand gemachten Codierungen wieder entfernen. Setzen Sie statt dessen jeweils einen Tabulator (oder einige Leerzeichen).
Mit folgenden 3 Befehlen können wir jetzt das Programm compilieren:
arm-linux-gnueabi-as test1.s
arm-linux-gnueabi-as -a test1.s
arm-linux-gnueabi-objcopy a.out -O binary test1.img
Statt "arm-linux-gnueabi" sollten Sie je nach installiertem Compiler was anderes eingeben.
Zum Beispiel könnte es "arm-none-eabi" sein.
Direkt auf dem Raspi kann man direkt "as" und "objcopy" verwenden.
Geben Sie erst mal nur den ersten Befehl ein. Der Compiler wird dann diverse Fehler reklamieren, z.B diese:
Error: internal_relocation (type: OFFSET_IMM) not fixed up
Error: garbage following instruction -- `mov r1,r6>>1'
Korrigieren Sie wie folgt:
  ldr r8, [pc,#Timer]  @ wird nicht erkannt
  ldr r8, Timer        @ so ist es korrekt

  mov r1, r6>>1        @ nicht erkannt
  mov r1, r6, lsr #1   @ korrekt

Wenn alle Fehler korrigiert sind, dann geben wir den zweiten Befehl ein:
arm-linux-gnueabi-as -a test1.s
Damit bekommen wir ein Listing unseres Programms mit Adressen und Codierungen. Die Codierungen werden dabei in falscher Reihenfolge angezeigt, also das niederwertigste Byte zuerst.
Jetzt können Sie kontrollieren ob der Compiler die richtigen Codes erzeugt hat.
Manchmal ist diese Kontrolle tatsächlich nötig, da der Compiler eventuell unsere Befehle anders interpretiert als wir es gemeint hatten. Zum Beispiel bei diesem Befehl:
        mov r0, #(1<<6*3)  @ GPIO16 gruene OK-LED
Damit der Compiler dieses korrekt versteht sind noch zusätzliche Klammern nötig:
        mov r0, #(1<<(6*3))  @ GPIO16 gruene OK-LED
Wenn alles korrekt ist, dann hat der Compiler eine Datei "a.out" erstellt.
Schauen Sie diese Datei mit "hdump a.out" an. Ausser dem Programmcode steht in dieser Datei noch eine Menge anderes Zeug.
Um das eigentliche Programm daraus zu extrahieren geben wir den dritten Befehl ein:
arm-linux-gnueabi-objcopy a.out -O binary test1.img
Jetzt können wir vergleichen mit dem was wir schon von Hand codiert hatten:
diff kernel.img test1.img
Wenn diff einen Unterschied feststellt, dann ist irgendwo noch der Wurm drin.

Um uns Tipparbeit zu ersparen beim Compilieren, erstellen wir uns dieses Makefile:

A=arm-linux-gnueabi-as
OBJCP=arm-linux-gnueabi-objcopy 
all: test1.img
test1.img: test1.s
        $A test1.s
        $(OBJCP) a.out -O binary test1.img
install: test1.img
        cp test1.img /media/boot/kernel.img
clean:
        rm -f *~ a.out
Dabei müssen die Einrückungen jeweils unbedingt mit einem Tabulator gemacht werden.
Der Name kann "Makefile" oder auch "makefile" sein.
Um neu zu compilieren müssen wir dann nur noch diesen Befehl eingeben:
make
Und zum auf der SD-Karte zu speichern noch dies:
make install
Wobei Sie sicherstellen sollten, dass im Makefile statt "/media/boot/" auch wirklich die richtige Disk steht (je nach System was anderes, auf Mac z.B. "/Volumes/boot/"). Es kann auch sein, dass statt "boot" eine Nummer steht.
Dies kann vor und nach einstecken der SD-Karte mit "df -h" überprüft werden.

Wenn Sie als Texteditor den emacs benutzen, können Sie jeweils direkt aus dem Editor heraus compilieren mit <CTRL>b.
Falls nicht schon gemacht sollten Sie dazu noch folgende Zeilen in ~/.emacs anfügen:

(global-set-key "\C-b" 'compile)
(global-set-key "\C-n" 'next-error)
Mit <CTRL>n kann man dann auch gleich zu allfälligen Fehlern springen.

Um nicht jedes Mal den Programmname im Makefile ändern zu müssen können wir jeweils einen Softlink setzen der auf das aktuelle Programm verweist. Wir benennen also unser bisheriges "test1.s" um in "blinken4b.s" und setzen den Softlink:

mv test1.s blinken4b.s
ln -s blinken4b.s test1.s

Jetzt geht die Post ab

Wir haben den ldr-Befehl bisher meist so verwendet, dass der Offset zuerst addiert wird, dann werden die Daten geladen und das Basisregister bleibt unverändert.
Um den neuen Wert im Basisregister zu speichern (also W-Bit im Code zu setzen) setzt man noch ein Ausrufezeichen. Dies wird auch Pre-increment genannt, also vor dem Lesen der Daten das Basisregister erhöhen und neuer Wert speichern.
Im Gegensatz dazu gibts noch das Post-increment, also wir wollen die Daten lesen (von der Adresse die im Basisregister steht), und erst danach das Basisregister noch um den Offset erhöhen. Dazu geben wir den Offset erst nach den eckigen Klammern an. Ein Ausrufezeichen (und setzen vom W-Bit) ist beim Postincrement nicht nötig, da ein Postincrement ohne im Basisregister zurückzuspeichern keinen Sinn machen würde.
Hier entsprechende Beispiele:
  ldr r1, [r4,#4]   @ gewoehnlicher ldr-Befehl: r1 von Adresse r4+4 lesen, r4 unveraendert
  ldr r1, [r4,#4]!  @ Pre-increment, also erst r4 erhoehen, dann lesen
  ldr r1, [r4],#4   @ Post-increment, zuerst lesen, dann r4 erhoehen

Mehrere Daten einlesen

Wir wollen nochmals das elegantere Einlesen der Zeiten im Beispiel blinken4.s untersuchen. Wie schon bei den Lösungen zu den Übungen erwähnt war das Problem, dass unser Programm nicht wie angenommen bei 0 gestartet wird sondern an der Adresse 0x8000 startet. Beim Einschalten des Raspberry ist ja unser Programm nicht wirklich das erste das ausgeführt wird, sondern zuerst startet mal ein Programm (auch Bootloader genannt), das die Datei "kernel.img" von der SD-Karte liest, dann im RAM speichert und schliesslich unser Programm startet. Die Adresse in die der Bootloader das Programm speichert ist nun mal 0x8000.

Also müssen wir r4 nicht auf 0x30 sondern auf 0x8030 setzen. Statt die Adresse von Hand auszurechnen und als ".word 0x8030" anzugeben, können wir die Berechnung dem Compiler überlassen und ".word Wartezeiten" schreiben.
Der entsprechende Programmteil sieht also so aus:

        ldr r4, adrWartezeiten   @ Die Adresse von Wartezeiten ins r4 laden, nicht der Inhalt
        ldr r0, [r4],#4          @ 1. Wartezeit einlesen
        ldr r1, [r4],#4          @ 2. Wartezeit einlesen
        ldr r2, [r4],#4          @ 3. Wartezeit einlesen
        ldr r3, [r4],#4          @ 4. Wartezeit einlesen

Wartezeiten:
        .word 500000       @ 0.5 Sekunden, also 1 Hz Blinkfrequenz
        .word 600000       @ 0.6 Sec, also etwas langsamer blinken, etwa 0.833 Hz
        .word 700000       @ 0.7 Sec, etwa 0.714 Hz
        .word 800000       @ 0.8 Sec, genau 0.625 Hz
adrWartezeiten:
        .word Wartezeiten  @ die Adresse von Wartezeiten
Entsprechend also im blinken4b.s gemacht, dann compilieren und das Ergebnis mit dem Disassembler angeschaut.
Und siehe da, die berechnete Adresse ist falsch! Es ist wieder 0x30 und nicht 0x8030.

Nun, das Problem ist, dass der Compiler die Startadresse unseres Programms nicht kennt. Das Compilieren eines Programms passiert meist in mehreren Schritten. Für grössere Projekte kann man mehrere Dateien separat compilieren zu sogenannten Objektdateien die meist die Endung ".o" haben. Diese Objektdateien werden dann mit dem Linker (der gewöhnlich ld heisst) zum endgültigen Programm zusammengefügt.
Der Linker ist dann dafür zuständig die endgültigen Adressen zu berechnen. Also müssen wir die Startadresse von 0x8000 dem Linker mitteilen.

Dazu müssen wir eine entsprechende Sektion definieren.
Wir erstellen dazu eine Textdatei die "sections.ld" heisst:

SECTIONS
 {
  .init 0x8000 : {*(.init)}
  .text : {*(.text)}
  .data : {*(.data)}
 }
Mit ".init" haben wir also die Start-Sektion definiert, dann noch text-Sektion für den Programmcode, und eine data-Sektion für unsere Daten.
Die letzten beiden Sektionen brauchen wir eigentlich noch nicht, werden aber später beim Ausführen der Programme unter Linux wichtig.

In unserem Programm müssen wir dann noch dieses ".init" anwenden. Dazu fügen wir ganz am Anfang dieses ein:

        .section .init
        .text
        .global _start
_start:
Eigentlich würde die erste Zeile reichen. Aber dann gibt der Compiler eine Warnung dass "_start" fehlt.

Zum compilieren müssen wir jetzt also dem Compiler (as) sagen, dass er nur bis zur Objektdatei gehen soll. Dem Linker (ld) sagen wir dann, dass er die Sektionen gemäss unserem "sections.ld" einrichten soll, um somit Adressen mit korrekter Startadresse zu berechnen.
Wir verwenden also diese Kommandos (auf dem Raspi):

as blinken4b.s -o blinken4b.o
ld blinken4b.o -t sections.ld -o blinken4b.elf
objcopy blinken4b.elf -O binary blinken4b.img
Jetzt können wir blinken4b.img mit dem Disassembler untersuchen.
Die Adresse sollte jetzt korrekt auf 0x8030 gesetzt sein.

Komfortableres Makefile

Wir erweitern also unser Makefile noch entsprechend, so dass die Startadresse stimmt.
Was auch noch gefehlt hat, ist ein Listing vom endgültigen Programm. Dies können wir nach dem Linken mit dem Befehl objdump machen.
Hier also das neue Makefile:
# von folgenden Zeilen nur eine nicht auskommentieren:
VOR=arm-linux-gnueabi-# fuer den Crosscompiler
#VOR=arm-none_eabi-# fuer anderen Crosscompiler
#VOR=# fuer direkt auf dem Raspberry

A=$(VOR)as
OBJCP=$(VOR)objcopy
L=$(VOR)ld
D=$(VOR)objdump

all: test1.img

test1.img: test1.s
	$A test1.s -o test1.o
	$L test1.o -t sections.ld -o test1.elf
	$D -d test1.elf >test1.list
	$(OBJCP) test1.elf -O binary test1.img

install: test1.img
	cp test1.img /media/boot/kernel.img

clean:
	rm -f *~ *.o a.out test1.elf test1.list test1.img
Im Makefile ist eine Zeile die mit # beginnt jeweils ein Kommentar. Wir benutzen dies um in den ersten Zeilen auszuwählen welchen Compiler wir gerade benutzen wollen.
Jetzt also compilieren und das endgültige Listing überprüfen:
make
more test1.list
Jetzt sind die Adressen korrekt: "_start" ist wirklich bei 0x8000, und bei "adrWartezeiten" ist korrekt 0x8030 eingetragen.

Um den Überblick nicht zu verlieren speichern wir mal alles in einem neuen Unterordner:

make clean
mkdir projekt1
cp blinken4b.s projekt1/
cp sections.ld projekt1/
cp makefile projekt1/
cd projekt1/
ln -s blinken4b.s test1.s
Wir werden später wieder auf dieses Projekt1 zurückkommen.

Assemblerprogramm unter Linux laufen lassen

Bisher sind unsere Assemblerprogramme so geschrieben, dass sie direkt laufen ohne ein Betriebssystem zu benutzen. Diese Art von Programmen wird auch "bare metal" genannt.
Wir wollen jetzt mal ein Assemblerprogramm auf dem Raspi unter Linux laufen lassen. Dabei müssen wir einige Dinge beachten. Zum Beispiel können wir nicht mehr direkt die Adressen für das GPIO und den Timer verwenden.
Damit sich Programme nicht gegenseitig stören können, wird jedem Programm jeweils ein eigener Speicherbereich zugewiesen. Versucht ein Programm auf einen anderen Bereich zuzugreifen wird sofort ein Interrupt ausgelöst um das Programm abzubrechen.
Damit wir auf den Speicherbereich vom GPIO zugreifen können müssen wir eine Anfrage ans Betriebssystem senden, dass es uns diesen Bereich reservieren soll. Das Betriebssystem veranlasst dann die MMU (Memory Management Unit) dazu den gewünschten Speicherbereich in einen Speicherbereich zu spiegeln der unserem Programm zugewiesen ist. Wir bekommen dann die Adresse dieses gespiegelten Bereichs zurück und können diese dann als GPIO-Basisadresse verwenden. Der Rest unseres Programms muss somit nicht verändert werden. Gleiches gilt auch für den Adressbereich des Timers.

Bisher habe ich Beispiele wie diese Anfrage zu machen ist nur in c gefunden.
Ich habe deshalb so ein Beispiel so angepasst, dass es die Anfrage macht, und dann unser Assemblerprogramm als Unterprogramm aufruft. Dabei übergibt es dem Unterprogramm (der Name ist "meinunterprogramm") zwei Werte, die der GPIO-Basisadresse und der Basisadresse des Systemtimers entsprechen.
Hier also das entsprechende c-Programm:

//  hauptprog.c  Hauptprogramm zum ein Assembler-Unterprogramm aufrufen

#define BCM2708_PERI_BASE        0x20000000
#define GPIO_BASE  (BCM2708_PERI_BASE + 0x200000) /* GPIO controller */
#define TIMER_BASE (BCM2708_PERI_BASE + 0x3000) /* System-Timer */
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define BLOCK_SIZE (4*1024)

// Set up a memory regions to access GPIO
unsigned int *setup_io(int base,int block_size)
{
 void *gpio_map;
 int  fd;
 if((fd=open("/dev/mem", O_RDWR|O_SYNC)) < 0)
  {
   fprintf(stderr,"Fehler bei open(\"/dev/mem\") fd=%d\n",fd);
   return NULL;
  }
 gpio_map = mmap(
      NULL,             //Any adddress in our space will do
      block_size,       //Map length
      PROT_READ|PROT_WRITE,// Enable reading & writting to mapped memory
      MAP_SHARED,       //Shared with other processes
      fd,               //File to map
      base         //Offset to GPIO peripheral
   );
 close(fd); //No need to keep fd open after mmap
 if(gpio_map==MAP_FAILED)
  {
   fprintf(stderr,"Fehler bei mmap(NULL, blocksize=%d, ... base=0x%X)\n",
           block_size, base);
   return NULL;
  }
 return (unsigned int*)gpio_map;
}

int main()
{
  int n;
  unsigned int *gpio_map,*timer_map;
  gpio_map=setup_io(GPIO_BASE,BLOCK_SIZE);
  timer_map=setup_io(TIMER_BASE,0x20);
  n=meinunterprogramm(gpio_map,timer_map);
  fprintf(stderr,"Rueckgabewert = %d = 0x%X\n",n,n);//test
  munmap(gpio_map,BLOCK_SIZE);
  munmap(timer_map,0x20);
  return n;
}
Nebenbei soll das Unterprogramm noch eine Zahl zurückgeben. Dies ist einfach das Register r0 wenn wir den Rücksprung machen.

Erstes Beispielprogramm unter Linux laufen lassen

Zuerst wollen wir ein etwas einfacheres Beispiel machen. Wir kopieren blinken4b.s nach blinken3.s, ersetzen das Unterprogramm pause durch die erste Version mit nur einem Vergleichsregister und machen einige Vereinfachungen, so dass nur noch 3 LEDs blinken.
Das Programm sollte dann etwa so aussehen:
@ blinken3.s   Blinken lassen von 3 LEDs als Binaerzaehler
        .section .init
        .text
        .global _start
_start:
        mov sp, #0x8000           @ Stack Pointer setzen
        mov r7, #0x20000000
        add r7, #0x200000         @ GPIO-Adresse im r7 behalten
        bl gpio_initialisieren
        ldr r0, Wartezeit
        mov r6, #0                @ Beim Start alle 3 LEDs aus
L1:     bl leds_gemaess_r6_schalten
        bl pause
        add r6, #1
        and r6, #0b111            @ nur unterste 3 Bits behalten
        b L1
Wartezeit: .word 500000       @ 0.5 Sekunden, also 1 Hz Blinkfrequenz

gpio_initialisieren:    @ 3 am GPIO angeschlossene LEDs auf Ausgang setzen
        push {r0,lr}         @ Register retten
        mov r0, #(1<<(6*3))  @ GPIO16 gruen OK-LED
        add r0, #(1<<(7*3))  @ GPIO17 rote LED
        str r0, [r7,#4]      @ benutzte GPIO im Bereich 10-19 setzen
        mov r0, #(1<<(7*3))  @ GPIO27 gelbe LED
        str r0, [r7,#8]      @ benutzte GPIO im Bereich 20-29 setzen
        pop {r0,pc}          @ gerettete Register zurueckholen, und Ruecksprung

Timeradr:
        .word 0x20003000    @ Timer-Basis-Adresse als konstanter Zahlenwert
pause:  @ Aufrufparameter: r0: soviele Microsekunden warten
        push {r4,r5,r8,lr}  @ Register retten
        ldr r8, Timeradr
        ldr r4, [r8,#4]     @ Zaehler des Timers einlesen, nur untere 32-Bit
        add r4, r0          @ Endzeitpunkt berechnen
        str r4, [r8,#12]    @ speichern im Vergleichsregister "Compare 0"
        mov r5, #1          @ Zum das Bit rueckzusetzen wirklich eine 1 schreiben!
        str r5, [r8,#0]     @ Bit M0 im CS ruecksetzen
L3:     ldr r5, [r8,#0]     @ neuer Status abfragen
        tst r5, #1          @ ist M0 gesetzt?
        beq L3              @ nein-> warten bis gesetzt
        pop {r4,r5,r8,pc}   @ grettete Register zurueckholen, und Ruecksprung

leds_gemaess_r6_schalten:   @ r6 unterste 3 Bits: bei gesetztem Bit soll entsprechende LED leuchten
        push {r0,r1,r6,lr}  @ Register retten
        eor r6, #0xFF       @ alle Bits umkehren, da LEDs mit 0 eingeschaltet werden
        mov r0, #17         @ GPIO17 1. LED, rot
        mov r1, r6          @ 1. Bit ins r1 kopieren
        bl gpio_ein_aus
        mov r0, #27         @ GPIO27 2. LED, gelb
        mov r1, r6, lsr #1  @ 2. Bit ins r1 kopieren
        bl gpio_ein_aus
        mov r0, #16         @ GPIO16 3. LED, gruene ok-LED
        mov r1, r6, lsr #2  @ 3. Bit ins r1 kopieren
        bl gpio_ein_aus
        pop {r0,r1,r6,pc}   @ gerettete Register zurueckholen, und Ruecksprung

gpio_ein_aus:  @ Parameter: r0=Nummer, r1.bit0=Spannung ein/aus (0=aus, 1=ein), r7=GPIO-Basisadresse
        push {r2,lr}        @ Register retten
        mov r2, #1
        lsl r2, r0          @ r2 = (1<<r0)
        tst r1, #1          @ ist erstes Bit in r1 gleich Null?
        streq r2, [r7,#40]  @ ja: Spannung aus
        strne r2, [r7,#28]  @ nein: Spannung ein
        pop {r2,pc}         @ gerettete Register zurueckholen, und Ruecksprung
Wir machen jetzt einen Unterordner "projekt2". Dort kopieren wir alles von "projekt1" hinein.
Dann noch das neue blinken3.s hineinkopieren und Softlink neu setzen:
mkdir projekt2
cp projekt1/* projekt2/
cp blinken3.s projekt2/
cd projekt2
rm test1.s
ln -s blinken3.s test1.s
Compilieren Sie wieder mit "make", kopieren dann auf die SD-Karte und probieren Sie aus ob es auch wirklich funktioniert. Überprüfen Sie mit der Stoppuhr ob die erste LED auch wirklich im Sekundentakt blinkt. Die zweite sollte alle 2 Sekunden, die dritte alle 4 Sekunden blinken.

Wenn alles korrekt funktioniert, sind wir bereit für den ersten Test unter Linux.
Ein Programm, das unter Linux gestartet wird läuft nicht mehr ab Startadresse 0x8000, sondern kann an eine beinahe beliebigen Adresse zu liegen kommen. Wir brauchen in diesem Fall dem Linker die Startadresse nicht mitzugeben. Diese wird beim Starten des Programms automatisch berechnet und alle Einträge, die absolute Adressen brauchen, entsprechend angepasst. Das funktioniert natürlich nicht mit der Imagedatei (test.img) sondern es braucht eine entsprechende ELF-Datei. (Wie unter Linux üblich jedoch ohne die Endung .elf)
Wir müssen also das Makefile entsprechend anpassen:

# von folgenden Zeilen nur eine nicht auskommentieren:
#VOR=arm-linux-gnueabi-# fuer den Crosscompiler
#VOR=arm-none_eabi-# fuer anderen Crosscompiler
VOR=# fuer direkt auf dem Raspberry

A=$(VOR)as
OBJCP=$(VOR)objcopy
L=$(VOR)ld
D=$(VOR)objdump
C=$(VOR)gcc
CFLAGS= -O2 -ggdb  #Optimierung (1-3 oder s), und Gnu-Debugger

#all: test1.img
all: test1

test1.img: test1.s
	$A test1.s -o test1.o
	$L test1.o -t sections.ld -o test1.elf
	$D -d test1.elf >test1.list
	$(OBJCP) test1.elf -O binary test1.img

test1: hauptprog.o unterprog.o
	$C hauptprog.o unterprog.o -o test1
	$D -d test1 >test1-linux.list
unterprog.o: test1.s
	$C $(CFLAGS) -c -DLINUX_VERSION test1.s -o unterprog.o
hauptprog.o: hauptprog.c
	$C $(CFLAGS) -c hauptprog.c -o hauptprog.o

install: test1.img
	cp test1.img /media/boot/kernel.img

clean:
	rm -f *~ *.o a.out test1.elf test1.list test1.img test1
Im Programm müssen wir auch einige kleine Änderungen machen. Zuerst dürfen wir den Stackpointer (sp) nicht setzen. Dieser ist schon vom Betriebssystem gesetzt. Dann wie oben erwähnt müssen wir für die Basisadressen vom GPIO und vom Timer, die Werte verwenden, die uns vom Hauptprogramm übergeben werden. Diese erhalten wir in den Registern r0 und r1.
Hier also die nötigen Änderungen:
@ blinken3.s   Blinken lassen von 3 LEDs als Binaerzaehler
.equ LINUX_VERSION, 1  @ auskommentieren fuer "bare-metal"

.ifdef LINUX_VERSION
        .text
        .global meinunterprogramm
meinunterprogramm:
        push {r1-r12,lr}
        mov r7, r0         @ GPIO-Adresse im r7 behalten
        str r1, Timeradr   @ Basisadresse des Timers bei Timeradr speichern
        bl meinstart
        mov r0, #0         @ Rueckgabewert
        pop {r1-r12,pc}
meinstart:
.else
        .section .init
        .text
        .global _start
_start:
        mov sp, #0x8000           @ Stack Pointer setzen
        mov r7, #0x20000000
        add r7, #0x200000         @ GPIO-Adresse im r7 behalten
.endif
        bl gpio_initialisieren
Also mit .ifdef .else .endif können wir zwischen 2 Blöcken auswählen, je nachdem ob wir das Programm normal, oder als Unterprogramm unter Linux compilieren wollen.
Es muss dann nur noch die Zeile .equ für normales Compilieren auskommentiert werden, oder eben nicht auskommentieren wenn es unter Linux laufen soll.

Nach erfolgreichem Compilieren mit "make" erhalten wir also die ausführbare Datei "test1".
Bevor Sie diese Datei ausführen wäre es eine gute Idee eine Sicherheitskopie von der System-SD-Karte zu machen. Selbstredend sollten keine andern Programme laufen, denn bei den ersten Versuchen werden Sie mit ziemlicher Sicherheit einige Systemabstürze produzieren.

Nach der Systemsicherung versuchen Sie also der Reihe nach diese Befehle um zu starten:

./test1
sudo ./test1
sudo gdb ./test1
Beim ersten werden Sie mit Sicherheit eine Fehlermeldung bekommen.
Beim zweiten Befehl erhalten Sie wahrscheinlich eine andere Fehlermeldung, oder das System stürzt ab.
Beim dritten Befehl bekommen wir den Prompt von gdb.
Geben Sie "help" ein um zu erfahren was es für Befehle im Debugger gibt.

Geben Sie "break meinunterprogramm" ein, danach "run". Mit "list" sollten Sie jetzt den Anfang unseres Assemblerprogramms sehen, so wie wir es geschrieben haben.
Mit "disass" bekommen wir auch ein Listing, aber so wie es der (in gdb eingebaute) Disassembler erkannt hat. Sie sehen dann auch einen Pfeil "=>" der auf den nächsten Befehl zeigt, der ausgeführt werden soll.
Mit "info register r0 r1 r7" können wir den Inhalt einiger Register anzeigen lassen.
Jetzt mit "step" oder "stepi" (weiss noch nicht was der Unterschied ist) den nächsten Befehl ausführen. Wieder mit "disass" sehen wir dann dass der Pfeil eins weiter gesetzt ist. Und nochmals "info register r7" um die Veränderungen zu sehen.
Jetzt wieder "step" - und wir bekommen eine Fehlermeldung (SIGSEGV).
Der Grund dafür ist, dass wir versucht haben innerhalb des Programmcodes (Sektion .text) Daten zu speichern. Nähmlich die Timeradresse wollten wir ja anpassen.
Aus Sicherheitsgründen ist Speichern innerhalb der Sektion ".text" nicht erlaubt. Dafür ist die Sektion ".data" da.
Das ist eigentlich kein Problem, wir wechseln vor "Timeradr:" mit ".data" in die entsprechende Sektion, und können dann vor "pause:" mit ".text" wieder zurückzuwechseln. Das Problem ist jetzt aber, dass wir mit den Befehlen ldr und str nicht mehr direkt darauf zugreifen können. Ich vermute, dass der Grund dafür ist, dass die data-Sektion zu weit entfernt sein könnte um mit der relativen Adressierung zuzugreifen. Jedenfalls können wir das umgehen indem wir die Adresse von "Timeradr" in der Code-Sektion speichern.
Die entsprechenden Programmteile sehen dann so aus:

        ldr r8, adrTimeradr  @ Adresse der Adresse laden
        str r1, [r8,#0]      @ Timer-Basisadresse in Timeradr speichern

        .data
Timeradr:
        .word 0x20003000    @ Timer-Basis-Adresse als konstanter Zahlenwert
        .text
adrTimeradr:
        .word Timeradr      @ die Adresse der Adresse des Timers

pause:  @ Aufrufparameter: r0: soviele Microsekunden warten
        push {r4,r5,r8,lr}  @ Register retten
        ldr r8, adrTimeradr @ Adresse der Adresse laden
        ldr r8, [r8,#0]     @ Timer-Basisadresse laden
        ldr r4, [r8,#4]     @ Zaehler des Timers einlesen, nur untere 32-Bit
Editieren Sie also das Programm entsprechend, neu compilieren und nochmals mit dem Debugger versuchen. Jetzt sollte eigentlich alles korrekt laufen.
Starten mit "sudo ./test1" sollte auch gehen.
Das Programm abbrechen können Sie mit <CTRL>c.

Übungen

  1. Verändern Sie blinken3.s so, dass es sich nach einer gewissen Zeit selbst beendet, und testen Sie ob ein Zahlenwert zurückzugeben funktioniert.
  2. Probieren Sie aus was passiert wenn Sie pause mit einer Warteschlaufe programmieren, statt den Timer zu verwenden.
  3. Versuchen Sie den Fehler in blinken4.s mit Hilfe des Debuggers zu finden. (ist schwierig zu finden, Debugger alleine reicht wahrscheinlich nicht)

Vielleicht enthält dieser 3.Teil noch Fehler.
Falls Sie welche finden, wäre ich um entsprechende Rückmeldungen dankbar (auch wenn es nur Tippfehler sind).


Letzte Änderungen

1.Feb.2015: Erstellung

Fortsetzung: Teil4

Kontakt-Formular

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