Programmieren in Maschinensprache auf dem Raspberry Pi - Teil4

Dies ist die Fortsetzung dieses Tutorials: Teil1 , Teil2 , Teil3

Lösungen zu den Übungen vom Teil3

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

  1. Eine mögliche Lösung ist: blinken3b.s
  2. Mit Warteschlaufe statt Timer blinkt es unter Linux viel schneller als wenn ohne Betriebssystem laufend. Offenbar kann man die Taktrate noch softwaremässig anpassen. Mit den bisherigen Programmen spielt die Geschwindigkeit aber keine Rolle, somit werden wir dieses Problem erst etwas später behandeln.
  3. Nach ausführlichen Tests mit dem Debugger ist die Schlussfolgerung, dass das Programm blinken4b.s korrekt ist. Trotzdem nicht korrekt blinkende LEDs lassen eigentlich nur einen Schluss zu: ein anderes Programm benutzt auch noch den Timer. Wir werden das noch in einem der nächsten Kapitel überprüfen.

Neue Schaltung mit 10 LEDs und 2 Taster

Um den Inhalt eines Registers auch ohne Debugger darzustellen könnten wir jetzt 32 LEDs installieren. Dazu würden wir aber mehr Ausgänge benötigen. Das könnte mit einem Schieberegister gelöst werden.
Wir können aber auch jeweils auf 8 LEDs ein Byte anzeigen und auf 2 weiteren LEDs dann noch welches Byte des Registers es ist. Um zwischen den einzelnen Bytes bequem zu wechseln wollen wir noch 2 Taster benutzen.
Wir bauen uns also die folgende kleine Schaltung:
schema10led2tast.gif zehnleds.jpg

(statt der teuren Adafruit-Platine geht eine billige Laborkarte genauso gut)
Die LEDs sind wieder gleich angeschlossen wie bei vorheriger Schaltung. Also wenn Ausgang Spannung hat, dann ist LED aus, wenn Ausgang auf 0 gesetzt, dann ist LED ein.
Zur besseren Übersicht habe ich die LEDs in zwei Vierergruppen und einer Zweiergruppe angeordnet.

Die beiden Taster sind Schalter, die Kontakt geben wenn gedrückt, und keinen Kontakt mehr wenn losgelassen.
Eigentlich müsste jeweils noch ein Widerstand zwischen Eingang und 3.3V geschaltet werden, so dass bei offenem Taster der Eingang Spannung erhält, bei gedrücktem Taster der Eingang auf 0 geht.
Ein Widerstand der die Spannung hochzieht wird als Pullup-Widerstand bezeichnet. So ein Pullup-Widerstand ist bereits im Raspberry eingebaut, und das für jeden GPIO-Eingang. Wir müssen diejenigen die wir brauchen nur noch per Software einschalten. Diese Widerstände sind in der Grössenordnung von 10kOhm.
Der Widerstand jeweils zwischen Eingang und Taster ist zur Sicherheit da, falls wir versehentlich den Eingang als Ausgang schalten und den Taster drücken. Damit verhindern wir dann einen Kurzschluss der den Raspberry zerstören könnte. Also unbedingt diese Widerstände (etwa 330 Ohm) einsetzen!

Taster abfragen

Die Offsetadresse für die GPIO-Eingänge 0 bis 31 ist 0x34, für 32 bis 53 dann 0x38.
Also einfach mit "ldr" einlesen und mit "tst" das entsprechende Bit testen.
Zuerst aber müssen wir noch die Pullup-Widerstände setzen. Dies ist beim Raspberry etwas umständlich.
Es gibt dazu drei relevante Offsets:
0x94  GPPUD       Was machen: 0=ausschalten, 1=Pulldown einschalten, 2=Pullup einschalten
0x98  GPPUDCLK0   Eingaenge 0 bis 31
0x9C  GPPUDCLK1   Eingaenge 32 bis 53
Es muss zuerst GPPUD gesetzt werden um zu definieren was zu machen ist. Pullup setzen, Pulldown setzen oder Widerstände ausschalten.
Dann muss mindestens 150 Takte gewartet werden.
Danach wird mit GPPUDCLK0 und/oder GPPUDCLK1 definiert welche Eingänge gesetzt werden sollen.
Dann muss wieder mindestens 150 Takte gewartet werden.
Danach müssen GPPUD, GPPUDCLK0 und GPPUDCLK1 wieder auf 0 gesetzt werden.
        mov r0, #2           @ Pullup-Widerstaende setzen (0=off, 1=pull down, 2=pull up)
        str r0, [r7,#0x94]   @ GPPUD
        bl wait150cycles
        mov r0, #(1<<25)     @ an GPIO-25 solle ein Pullup eingeschaltet werden
        add r0, #(1<<24)     @ und auch an GPIO-24
        str r0, [r7,#0x98]   @ GPPUDCLK0 (fuer GPIO-00 bis 31)
        bl wait150cycles
        mov r0, #0           @ alle GPPUD-Register wieder auf 0 setzen
        str r0, [r7,#0x94]   @ GPPUD
        str r0, [r7,#0x98]   @ GPPUDCLK0
        ...
wait150cycles:        @ warte mindestens 150 Takte
        push {r0,lr}
        mov r0, #50
wc1:    subs r0, #1   @ Die Schlaufe braucht wahrscheinlich 3 Takte
        bne wc1       @ also 50*3 = 150 Takte total
        pop {r0,pc}
Dies fügen wir im Unterprogramm "gpio_initialisieren" ein.

Den Zustand eines Tasters einlesen ist danach einfach:

        ldr r0, [r7,#0x34]   @ GPIO-Inputs
        tst r0, #(1<<25)     @ Eingang GPIO-25 auf 0?
        beq gedrueckt        @ ja--> Taste gedrueckt

Tastendrücke zählen

Wir erstellen wieder einen neuen Unterordner:
mkdir projekt3
cp projekt2/makefile projekt3/
cp projekt2/sections.ld projekt3/
cp blinken4b.s projekt3/taster.s
cd projekt3
ln -s taster.s test1.s
Jetzt in taster.s alles unnötige entfernen, "gpio_initialisieren" und "gpio_ein_aus" erweitern für die neu angeschlossenen LEDs, und obiger Programmteil zum Pullup-Widerstände setzen einfügen.
Dann folgendes Hauptprogramm erstellen:
_start: mov sp, #0x8000         @ Stack Pointer setzen
        mov r7, #0x20000000
        add r7, #0x200000       @ GPIO-Adresse im r7 behalten
        bl gpio_initialisieren
        mov r6, #0              @ alle LEDs aus
L1:
        ldr r0, [r7,#0x34]      @ GPIO-Inputs
        tst r0, #(1<<25)        @ Eingang GPIO-25 auf 0?
        bleq taster1            @ ja: bei Tastendruck Zaehler erhoehen
        ldr r0, [r7,#0x34]      @ GPIO-Inputs
        tst r0, #(1<<24)        @ Eingang GPIO-24 auf 0?
        bleq taster2            @ ja: bei Tastendruck Zaehler erniedrigen
        bl leds_gemaess_r6_schalten
        b L1
taster1:
        push {lr}
        add r6, #1
        b tast_loslassen
taster2:
        push {lr}
        sub r6, #1
tast_loslassen:        
        ldr r0, [r7,#0x34]       @ GPIO-Inputs
        and r0, #(1<<24)|(1<<25) @ einer der Taster noch gedrueckt?
        cmp r0, #(1<<24)|(1<<25) @ beide losgelassen?
        bne tast_loslassen       @ nein-> warten bis losgelassen
        pop {pc}
Wenn die Taster perfekt funktionieren, dann wird mit dem einen hochgezählt, mit dem andern runtergezählt. Die meisten Taster sind aber nicht perfekt, sondern haben ein Prellverhalten (Google-Stichwort Tastenentprellung). Dann wird bei einem Tastendruck jeweils um mehrere weitergezählt.
Die einfachste Methode das zu verhindern ist, nach jedem erkannten Tastendruck jeweils eine kurze Pause einzufügen.

Jetzt habe wir alle Grundlagen um ein Unterprogramm zu schreiben, das uns den Inhalt eines Registers anzeigt. Machen Sie das mal als Übung.
Geben Sie dem Unterprogramm den Namen "r0_auf_LEDs_darstellen" und sorgen Sie dafür, dass man mit dem einen Taster zwischen den Bytes wechseln kann. Beim drücken des andern Tasters sollte das Unterprogramm verlassen werden, damit das Hauptprogramm einen andern Wert ins r0 schreiben kann, der danach angezeigt werden soll. Machen Sie die Warteschleife nach erkanntem Tastendruck ohne die Vergleichsregister des Timers zu verwenden (Timer verwenden ist ok, aber keine Vergleichsregister setzen).

Inhalt der Timer-Vergleichsregister anzeigen

Da wir ja schon stark vermuten dass der Timer noch von einem andern Programm benutzt wird, wollen wir mal schauen, wie sich die Vergleichsregister verändern, wenn unser Programm nichts daran ändert.
@  timreganz.s
_start: mov sp, #0x8000           @ Stack Pointer setzen
        mov r7, #0x20000000
        add r7, #0x200000         @ GPIO-Adresse im r7 behalten
        mov r8, #0x20000000
        add r8, #0x3000           @ Timer-Basisadresse im r8 behalten
        bl gpio_initialisieren
L0:     mov r1, #0                @ Zaehler fuer Offset
L1:     mov r2, r8                @ r2 = Timerbasis + Offset
        add r2, r1                @ Offset soll von 0 bis 0x18 laufen
        ldr r0, [r2]
        bl r0_auf_LEDs_darstellen @ alle Timer-Register nacheinander anzeigen
        add r1, #4                @ naechster Offset
        cmp r1, #0x1C             @ Maximalwert ueberschritten?
        beq L0                    @ ja-> wieder bei 0 beginnen
        b L1   
Wenn alles korrekt läuft sollte jetzt beim Starten zuerst das Timer-Register CS angezeigt werden, dann untere 32 Bits des Zählers, dann obere 32 Bits, dann nacheinander die Vergleichsregister.
Wenn Sie den Zähler beobachten, werden Sie feststellen dass nicht weitergezählt wird. Klar, wir haben ja einen momentanen Zählerstand in r0 dem Unterprogramm übergeben, und ändern diesen Wert dann nicht mehr.
Um wirklich jeweils aktuelle Werte zu sehen, kopieren Sie das Unterprogramm und geben ihm den neuen Namen "speicher_bei_r2_auf_LEDs_darstellen". Dann verschieben Sie den Befehl "ldr r0, [r2]" in das Unterprogramm, so dass während auf Tastendrücke wartend, der Wert laufend aktualisiert wird.
Das Hauptprogramm dann auch entsprechend angepasst:
        ...
        add r2, r1                @ Offset soll von 0 bis 0x18 laufen
        bl speicher_bei_r2_auf_LEDs_darstellen
        ...
Jetzt sollte also wenn Sie die untern 32 Bits des Zählers beobachten, die LEDs entsprechend hochzählen. (Beim untersten Byte scheinen einfach alle LEDs zu leuchten, da es zu schnell für unsere Augen ist.)
Wenn Sie jetzt das CS genau beobachten, werden Sie feststellen dass die erste und die dritte LED schwach flackern. Also benutzt das Hintergrundprogramm die Vergleichsregister 0 und 2. Wenn Sie die Vergleichsregister betrachten werden Sie feststellen, dass genau diese beiden sich ständig ändern, die andern beiden jedoch auf 0 bleiben.
Das Hintergrundprogramm, das den Timer benutzt ist der Grafikcontroller (GPU). Dieser läuft unabhängig vom Hauptprozessor (CPU), den wir programmieren.
Schlussfolgerung ist also: wir können nur die Vergleichsregister 1 und 3 sinnvoll benutzen, die Vergleichsregister 0 und 2 sollten wir unangetastet lassen.

Fähnchen setzen

Bisher haben wir erst die beiden Flags "Zero" (kurz Z) und "Carry" (kurz C oder Cy) benutzt. Es gibt aber noch zwei wichtige Flags: "Neg" (kurz N) und "Overflow" (kurz V).
Im Teil1 bei der Tabelle der Bedingungen steht meistens noch in Klammern welchem Flag die Bedingung entspricht.

All diese Flags und noch ein paar andere sind im Register "CPSR" gespeichert:

 NZCV  Q00J  0000  G3G2G1G0  0000  00EA  IFTM4  M3M2M1M0

 N: Neg                                  I: IRQ-Disable
 Z: Zero                                 F: FIQ-Disable
 C: Carry                                M4-M0: Modus
 V: Overflow     JT: 00=ARM,01=Thumb,10=Jazelle,11=Reserved
 Q: ?            G3-G0: ??   EA: ?
Die Flags NZCV sind also die Bedingungs-Flags.
Mit I und F kann man Interrupts erlauben oder sperren. Wir kommen gleich noch darauf zu sprechen.
M4-M0 ist der aktuelle Modus. Beim Programmstart ist der SVC-Modus (Supervisor, 0x13) gesetzt. In Interrupts ist Modus IRQ (InterrupReQuest, 0x12) oder FIQ (FastInterrupreQest, 0x11) gesetzt. Der Usermodus (0x10) ist vermutlich aktiv wenn von Linux gestartet. Auch das schauen wir gleich noch genauer an.
Die Flags J und T definieren welcher Maschinensprache-Satz aktiv ist. Wir benutzen immer ARM, also immer auf 00 gesetzt. Thumb währe ein anderer Assembler-Befehlssatz der jeweils nur 16 Bit pro Befehl braucht, statt 32 Bit beim ARM-Befehlssatz. Wenn wir ein Unterprogramm schreiben wollten für ein übergeordnetes Programm das im Thumb-Befehlssatz geschrieben ist, dann müssten wir mit dem Befehl "bx lr" zurückkehren. Diesen Befehl könnte man auch als Rücksprung verwenden wenn vom ARM-Befehlssatz aufgerufen. Dann hat es die gleiche Wirkung wie "mov pc,lr". Also wenn unklar ist welcher Befehlssatz das übergeordnete Programm benutzt, dann "bx lr" verwenden.
(Die Bedeutung von Q, G3-G0 und EA kenne ich auch noch nicht)
Ausser dem CPSR gibts noch das SPSR, das bei Interrupts verwendet wird um das CPSR zwischenzuspeichern. (fast jeder Modus hat ein eigenes SPSR)

Befehle um CPSR zu beeinflussen

Bisher konnten wir nur die Bedingungs-Flags setzen indem wir das s-Bit im Befehl gesetzt hatten.
Um die andern Flags zu lesen und zu setzen gibt es noch diese Befehle:
  mrs r1, cpsr      @ cpsr nach r1 kopieren (Move Register from Status)
  msr cpsr, r1      @ r1 nach cpsr kopieren (Move Status from Register)
  msr cpsr_ctl, r1  @ nur unterstes Byte neu setzen
Wir können jetzt ein kleines Programm schreiben, um zu überprüfen wie die Flags und der Modus gesetzt sind. Bei dieser Gelegenheit überprüfen wir auch gleich noch welche Register im IRQ-Modus (den wir im nächsten Abschnitt benutzen wollen) separat vorhanden sind (Register-Banking).
@  cpsrcheck.s
_start:	mov sp, #0x8000           @ Stack Pointer setzen
	mov r7, #0x20000000
        add r7, #0x200000         @ GPIO-Adresse im r7 behalten
        mov r8, #0x20000000
        add r8, #0x3000           @ Timer-Basisadresse im r8 behalten
	bl u1
u1:     mov r6, lr                @ lr im r6 sichern
        bl gpio_initialisieren
	mrs r1, cpsr              @ test
	msr spsr, r1              @ test: CPSR nach SPSR kopieren
L1:     mov r0, sp
        bl r0_auf_LEDs_darstellen @ aktueller SP anzeigen, 0x8000 (unter Linux anders)
	mov r0, r6
        bl r0_auf_LEDs_darstellen @ aktuelles LR anzeigen, 0x8018 (unter Linux anders)
	mov lr, r6                @ lr wieder auf gesicherten Wert setzen
        mov  r0, #0x80000000
        adds r0, #0x80000000      @ Flags setzen: N=0, Cy=1, Z=1, V=1
        mrs r1, cpsr              @ CPSR lesen
        mrs r2, spsr              @ SPSR lesen (eventuell zwischengespeichertes CPSR)
        mrs r0, cpsr              @ aktueller Modus in r0 speichern
        mov r3, #0xD2             @ auf Modus IRQ umschalten (0x12 + Disable-Bits)
        msr cpsr_ctl, r3          @ unterstes Byte von CPSR setzen
        mrs r3, spsr              @ SPSR des IRQ-Modus lesen
        mov r4, sp                @ SP des IRQ-Modus lesen
        mov r5, lr                @ LR des IRQ-Modus lesen
        msr cpsr_ctl, r0          @ wieder in vorherigen Modus zurueckschalten
        mov r0, r1
        bl r0_auf_LEDs_darstellen @ CPSR anzeigen
        mov r0, r2
        bl r0_auf_LEDs_darstellen @ SPSR anzeigen
        mov r0, r3
        bl r0_auf_LEDs_darstellen @ SPSR vom IRQ-Modus anzeigen
        mov r0, r4
        bl r0_auf_LEDs_darstellen @ SP vom IRQ-Modus anzeigen
        mov r0, r5
        bl r0_auf_LEDs_darstellen @ LR vom IRQ-Modus anzeigen
        b L1
Ich habe hier das lr ausnahmsweise im r6 gesichert statt mit push. Sonst wissen wir nach den Unterprogrammaufrufen nicht mehr wie lr gesetzt sein sollte. Unter Linux können Sie dann mit "mov pc, r6" zurückkehren (statt mit "b L1" unendlich in der Schlaufe zu bleiben).
Lassen Sie das Programm normal laufen (bare metal), und auch unter Linux, um die Unterschiede festzustellen.
Sie werden dann sehen, dass mit "bare metal" der SVC-Modus, unter Linux hingegen der User-Modus läuft. Ausserdem scheinen unter Linux die beiden SPSR identisch zu sein, bei "bare metal" jedoch eindeutig verschieden. SP und LR hingegen sind in beiden Fällen jeweils unterschiedlich.
(wobei mir gerade nicht klar ist was "mrs r2, spsr" im User-Modus wirklich macht, denn da gibt es angeblich gar kein SPSR)

Interrupts (Unterbrechungen)

Oft kommt es vor, dass in einem Programm auf ein Ereignis gewartet wird. Zum Beispiel warten wir bis der Timer einen bestimmten Wert erreicht hat. So wie wir das bisher machen, also dauernd den Timer lesen und mit dem Endwert vergleichen, ist es ziemlich ineffizient.

Die CPU hat deshalb einen speziellen Eingang, der vergleichbar mit einer Türklingel ist. Sobald dieser Eingang gesetzt ist, wird das laufende Programm unterbrochen und eine sogenannte Interrupt-Routine aufgerufen. Ähnlich wie wenn wir am arbeiten sind, dann durch die Türklingel unterbrochen werden. Dann machen wir kurz was anderes, nämlich an die Tür gehen, nachschauen wer da ist, und entsprechend handeln (z.B. Packet vom Pöstler entgegen nehmen). Die Interrupt-Routine schaut ebenfalls nach, was der Grund der Unterbrechung war, handelt dann entsprechend und kehrt danach zum ursprünglichen Programm zurück.

Genau genommen gibt es mehrere Interrupt-Eingänge. Wir wollen vorerst aber nur den "IRQ" betrachten. Wenn dieser Interrupt ausgelöst wird, dann wird statt dem aktuellen Befehl, den die CPU ausführen sollte, zuerst ein "bl 0x18" gemacht. Das heisst es wird das Unterprogramm an der absoluten Adresse 0x18 abgearbeitet. Dabei wird vom aktuellen Modus automatisch in den IRQ-Modus gewechselt und im SPSR das ursprüngliche CPSR gesichert. Das LR ist auch speziell, es zeigt bereits auf den nächsten Befehl, statt auf den noch nicht abgearbeiteten Befehl bei dem das Hauptprogramm unterbrochen wurde. Beim Rücksprung muss deshalb noch 4 subtrahiert werden. Ausserdem muss das s-Bit gesetzt werden, das in diesem Fall dafür sorgt dass das in SPSR gesicherte CPSR wieder zurückgeschrieben wird.
Der Rücksprungbefehl muss deshalb genau so lauten:

        subs pc, lr, #4

Wir erstellen wieder wie üblich einen neuen Unterordner "projekt4".
Kopieren Sie eins der funktionierenden Programme von Projekt3 dort hinein, dann umbenennen in "irqtest1.s" und Hauptteil ersetzen durch unser erstes Interrupt-Beispiel.

Dieses erste Beispiel soll möglichst einfach sein um das Prinzip zu erklären. Es soll beim Starten die erste LED (rot) leuchten, dann nach 5 Sekunden soll ein Interrupt ausgelöst werden, womit dann die zweite LED (gelb) leuchten soll.

@ irqtest1.s  einfachstes Beispiel mit Interrupt
        .section .init
        .text
        .global _start
_start: mov sp, #0x8000         @ Stack Pointer setzen
        mov r7, #0x20000000
        add r7, #0x200000       @ GPIO-Adresse im r7 behalten
        mov r8, #0x20000000
        add r8, #0x3000         @ Timer-Basisadresse im r8 behalten
        mov r9, #0x20000000
        add r9, #0xB000         @ InterruptController-Basisadresse
        bl gpio_initialisieren
        bl pause_set
        bl irq_init
        mov r6, #1              @ rote LED
L1:     bl leds_gemaess_r6_schalten
        b L1

irq_routine:
        push {r0,lr}
        mov r6, #2              @ gelbe LED setzen, r6 global benutzt
        mov r0, #0b10           @ M1
        str r0, [r8,#0]         @ Bit M1 im CS ruecksetzen
        pop {r0,lr}
        subs pc, lr, #4         @ Rueckkehren von IRQ

adr_irq_routine:
        .word irq_routine

irq_init:
        push {r0-r2,lr}
        mrs r0, cpsr            @ aktueller Modus in r0 sichern
        mov r1, #0xD2           @ in IRQ-Modus schalten
        msr cpsr_ctl, r1
        mov sp, #0x6000         @ Stack Pointer fuer IRQ-Modus setzen
        msr cpsr_ctl, r0        @ in vorherigen Modus zurueck
        mov r2, #0x18           @ Adresse des irq-Interrupt-Vectors
        ldr r1, adr_irq_routine @ Adresse der irq_routine
        sub r1, r2
        sub r1, #8              @ Berechung der Sprungdistanz
        lsr r1, #2              @ Dividieren durch 4 fuer Distanz in 4-Byte-Worten
        add r1, #0xEA000000     @ Code des Sprungbefehls addieren
        str r1, [r2]            @ irq-Interrupt-Vector setzen
        @ Flags zum IRQ erlauben setzen, im CPSR und im Interrupt-Controller:
        mrs r0, cpsr
        bic r0, #0x80           @ IRQ-Disable auf 0 setzen
        msr cpsr_ctl, r0
        mov r1, #(1<<1)         @ IRQ 1 erlauben fuer Systemtimer Compare1
        str r1, [r9,#0x210]     @ in "Enable IRQs 1" entsprechendes Bit setzen
        pop {r0-r2,pc}

Wartezeit: .word 5000000        @ Anzahl Microsekunden, also 5 Sekunden

pause_set:
        push {r0,r2,lr}
        ldr r0, Wartezeit
        ldr r2, [r8,#4]         @ Zaehler des Timers einlesen, nur untere 32-Bit
        add r2, r0              @ Endzeitpunkt berechnen
        str r2, [r8,#16]        @ speichern im Vergleichsregister "Compare 1"
        mov r2, #0b10           @ M1
        str r2, [r8,#0]         @ Bit M1 im CS ruecksetzen
        pop {r0,r2,pc}
Dieses Programm bedarf noch einiger Erklährungen.
"pause_set" sollte klar sein. Wir setzen es so dass nach 5 Sekunden M1 gesetzt wird. Damit dann ein Iterrupt ausgelöst wird, müssen wir ein spezielles Bit im Interrupt-Controller setzen, der im BCM2835 enthalten ist. Die entsprechenden Adressen sind auf Seite 112 im Datenblatt zu finden. Also Basisadresse ist 0x2000B000, und Offset für unter aderem Timer-IRQ-Bits ist 0x210. Die ersten 4 Bits entsprechen den Timer-Vergleichsregistern. Wobei, wie schon festgestellt, 2 davon von der GPU benutzt werden. Dies ist im Datenblatt unvollständig dokumentiert. Überhaupt alles was die GPU betrifft wurde offenbar absichtlich weggelassen.

Im Unterprogramm "irq_init" setzen wir also das entsprechende Bit für "Compare1". Ausserdem setzen wir auch noch das IRQ-Disable-Bit im CPSR auf 0 um Interrupts zu erlauben.

Das erste jedoch was in "irq_init" gemacht werden muss, ist das Interrupt-Programm an der absoluten Adresse 0x18 zu setzen. Dort ist aber nur ein einziges Wort benutzbar, denn an der Adresse 0x1C kommt bereits ein anderes Interrupt-Programm (FIQ). Wir setzen deshalb also an Adresse 0x18 nur einen Sprungbefehl auf unsere Interrupt-Routine ein.

Ausserdem, da es ja ein separates SP im Interrupt-Modus gibt, müssen wir auch noch dieses setzen. Es darf jedoch keinesfalls gleich wie das normale SP sein, sonst gibts ein fürcherliches Durcheinander. (Bei sehr grossen Programmen sollte noch genauer überlegt werden wie die beiden SP zu setzen sind, so dass sie auch bei sehr tief verschachtelten Unterprogrammen niemals überlappen.)

In der Interrupt-Routine "irq_routine" setzen wir einfach nur r6 auf den Wert um die gelbe LED einzuschalten. Dabei müssen wir dann darauf achten, dass wir r6 nirgends im restlichen Programm verändern.
Ausserdem ist es noch wichtig die Interruptquelle wieder abzuschalten. Sonst würde beim Rücksprung gleich wieder die Interruptroutine aufgerufen, und das Hauptprogramm währe dann blockiert. In unserem Fall schalten wir die Interruptquelle wieder aus indem wir das M1 Bit im CS des Timers wieder rücksetzen.

Wenn wir mehrere Interruptquellen eingeschaltet haben, dann müssen wir natürlich in der Interruptroutine noch überprüfen welche Quelle den Interrupt ausgelöst hat. Also wenn wir z.B. noch die Eingänge der Taster mit dem Interrupt abfragen, dann könnte der Anfang der Interruptroutine etwa so aussehen:

irq_routine:
        push {r0-r2,lr}
        mov r0, [r8]
        tst r0, #0b10       @ M1 gesetzt?
        bne irq_timer       @ ja-->
        ldr r0, [r7,#0x34]
        tst r0, #(1<<24)    @ Taster GPIO24 gedrueckt?
        beq irq_taster1     @ ja-->

Übungen

  1. Erstellen Sie eine Programm, das auf einem noch freien GPIO-Ausgang ein symmetrisches Rechtecksignal von exakt 10kHz ausgibt. Machen Sie es so, dass im Hauptprog alle Register uneingeschränkt benutzbar bleiben.
  2. Kombinieren Sie das 10kHz-Programm mit timreganz.s oder cpsrcheck.s
  3. Versuchen Sie auch die Tasterzustände mit Hilfe von Interrupts zu setzen.


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


Letzte Änderungen

25.Feb.2015: Erstellung

Fortsetzung: Teil5 (... folgt hier vielleicht bald ...)

Kontakt-Formular

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