apt-get install gcc-arm-linux-gnueabi
port install arm-none-eabi-gcc port install arm-none-eabi-binutils
cd tutorial mkdir teil3 cd teil3 cp ../blinken4.s test1.sDann müssen wir noch die bereits von Hand gemachten Codierungen wieder entfernen. Setzen Sie statt dessen jeweils einen Tabulator (oder einige Leerzeichen).
arm-linux-gnueabi-as test1.s arm-linux-gnueabi-as -a test1.s arm-linux-gnueabi-objcopy a.out -O binary test1.imgStatt "arm-linux-gnueabi" sollten Sie je nach installiertem Compiler was anderes eingeben.
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
arm-linux-gnueabi-as -a test1.sDamit bekommen wir ein Listing unseres Programms mit Adressen und Codierungen. Die Codierungen werden dabei in falscher Reihenfolge angezeigt, also das niederwertigste Byte zuerst.
mov r0, #(1<<6*3) @ GPIO16 gruene OK-LEDDamit der Compiler dieses korrekt versteht sind noch zusätzliche Klammern nötig:
mov r0, #(1<<(6*3)) @ GPIO16 gruene OK-LEDWenn alles korrekt ist, dann hat der Compiler eine Datei "a.out" erstellt.
arm-linux-gnueabi-objcopy a.out -O binary test1.imgJetzt können wir vergleichen mit dem was wir schon von Hand codiert hatten:
diff kernel.img test1.imgWenn 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.outDabei müssen die Einrückungen jeweils unbedingt mit einem Tabulator gemacht werden.
makeUnd zum auf der SD-Karte zu speichern noch dies:
make installWobei 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.
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
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
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 WartezeitenEntsprechend also im blinken4b.s gemacht, dann compilieren und das Ergebnis mit dem Disassembler angeschaut.
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.
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.imgJetzt können wir blinken4b.img mit dem Disassembler untersuchen.
# 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.imgIm 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.
make more test1.listJetzt 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.sWir werden später wieder auf dieses Projekt1 zurückkommen.
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.
@ 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 RuecksprungWir machen jetzt einen Unterordner "projekt2". Dort kopieren wir alles von "projekt1" hinein.
mkdir projekt2 cp projekt1/* projekt2/ cp blinken3.s projekt2/ cd projekt2 rm test1.s ln -s blinken3.s test1.sCompilieren 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 test1Im 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.
@ 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_initialisierenAlso 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.
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 ./test1Beim ersten werden Sie mit Sicherheit eine Fehlermeldung bekommen.
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-BitEditieren Sie also das Programm entsprechend, neu compilieren und nochmals mit dem Debugger versuchen. Jetzt sollte eigentlich alles korrekt laufen.
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).