(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!
0x94 GPPUD Was machen: 0=ausschalten, 1=Pulldown einschalten, 2=Pullup einschalten 0x98 GPPUDCLK0 Eingaenge 0 bis 31 0x9C GPPUDCLK1 Eingaenge 32 bis 53Es muss zuerst GPPUD gesetzt werden um zu definieren was zu machen ist. Pullup setzen, Pulldown setzen oder Widerstände ausschalten.
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
mkdir projekt3 cp projekt2/makefile projekt3/ cp projekt2/sections.ld projekt3/ cp blinken4b.s projekt3/taster.s cd projekt3 ln -s taster.s test1.sJetzt 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.
_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.
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).
@ 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 L1Wenn 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.
... 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.)
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.
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 setzenWir 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 L1Ich 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).
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.
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-->