Pfad: Home ==> Mikrobeginner ==> 6. LED mit Interrupt     This page in English (external): Flag EN
ATtiny13

Lektion 6: Eine LED blinkt mit dem Interrupt


Mit dieser Lektion treten wir schon in die Interrupt-Programmierung ein und beenden die lineare Programmierung. Nur sehr einfache Aufgaben lassen sich ohne Interrupts erledigen. Am häufigsten sind mehrere Aufgaben (scheinbar) gleichzeitig zu erledigen, was nur mit Interrupts vernünftig geht. Und genau das soll hier als grundlegende Programmiertechnik erlernt werden.

6.0 Übersicht

  1. Einführung in die Interrupt-Programmierung
  2. Hardware, Bauteile, Aufbau
  3. Timer mit Overflow-Interrupt
  4. Timer mit CTC-Interrupt

6.1 Einführung in die Interrupt-Programmierung

6.1.1 Interrupts

Der Vorteil der Interrupt-Programmierung ist, dass mehrere zeitkritische Vorgänge gleichzeitig ablaufen und trotzdem alle eintretenden Ereignisse korrekt ablaufen und bearbeitet werden. Spätestens dies ist das Ende aller Warte- und Verzögerungsschleifen.

Interrupts sind automatisierte Unterbrechungen des Programms. Läuft beispielsweise der Timer über (von FF auf 00), können wir mittels eines Bits im Timer veranlassen, dass bei diesem Ereignis ein Interrupt ausgelöst wird. Ist das Bit gesetzt, dann bewirkt jeder Überlauf Was jetzt weiter passiert, ist Sache der selbstgeschriebenen Interrupt-Service-Routine. Dort wird erledigt, was beim Eintreten des Interrupts zu erledigen ist. Nach Erledigung kehrt der Programmablauf wieder dahin zurück, wo er zum Zeitpunkt der Unterbrechung war.

Bei diesem Konzept müssen zwei Bedingungen sichergestellt werden. Erstens darf während der Ausführung der Interrupt-Service-Routine nicht irgend ein anderer Interrupt dessen Ausfürung unterbrechen (verschachtelter Interrupt). Daraus resultiert zweitens, dass bei gleichzeitig eintretenden Unterbrechungen eine Reihenfolge festgelegt werden muss, wer zuerst mit der Bearbeitung dran ist (Priorität).

I-Bit Die erste Bedingung wird durch ein Flaggenbit erfüllt, das im Statusregister SREG, einem Port, lokalisiert ist. Es heißt "I" und liegt im Bit 7 des Ports. Mit einer Null in diesem Bit wird kein Interrupt ausgeführt, welche Hardware auch immer nach Unterbrechung ruft. Es muss daher nach dem Initiieren der Hardware auf Eins gesetzt werden, damit überhaupt ein Interrupt ernstgenommen wird. Das Eins-Setzen dieses Bits geht mit der Instruktion "SEI" (SEt I), sein Löschen mit "CLI" (CLear I).

Genau dieses Bit wird automatisch auf Null gesetzt, sobald der entsprechende Interrupt in Bearbeitung geht (und der Programmablauf verzweigt). Ist die Service-Routine beendet, muss sie dieses Bit wieder auf Eins setzen, um weitere anstehende oder neu eintretende Unterbrechungen wieder zuzulassen. Ohne dieses Setzen würden gar keine weiteren Unterbrechungen mehr zugelassen, die einsamen Rufer nach Unterbrechung würden verhungern.

Damit wird auch klar, weshalb im obigen Ablauf jeder Unterbrecher ein Bit setzen muss, wenn der Fall eintritt: es kann durchaus sein, dass er erst später dran ist, wenn andere Interrupts noch in Bearbeitung sind. Und so lange signalisiert dieses Bit eben Unterbrechungsbedarf. Damit ist auch die Priorisierung von Interrupts klar: sie ist notwendig um zu bestimmen, wer als erstes dran ist, wenn mehrere dieser Bits gleichzeitig Unterbrechungsbedarf signalisieren. Die Priorität ist fest (siehe unten) und nicht beeinflussbar. Je höher der Interrupt in der Vektorenliste steht (siehe weiter unten), desto höher die Priorität.

Weil jede Interrupt-Service-Routine den weiteren Fortgang der Bearbeitung erst mal blockiert, gilt als wichtigste Regel, diese Service-Routine so kurz und knapp wie möglich zu halten. Keine Zeit hier, um langwierige Operationen oder Verzögerungsschleifen unterzubringen. Optimalerweise beschränkt sich diese Routine auf 10 bis 15 Instruktionen. Langwierige Bearbeitungen werden nach außerhalb dieser Routine zu verlegen sein. Das bedeutet, dass die Interrupt-Service-Routine mit "außerhalb" über Flaggen kommunizieren muss. Mehr dazu später.

6.1.2 Stackspeicher

Um die Bearbeitung von Interrupts zu ermöglichen, braucht es einen Speicher, wo die Unterbrechungsadresse zeitweise abgelegt werden kann. Dazu verwendet der AVR den sogenannten Stack (Stapel) im statischen RAM. Der ATtiny13 hat 64 Bytes SRAM, reichlich um die zwei Byte lange Adresse abzulegen. Der Stapel wird immer am Ende des Speichers angelegt, indem der Stapelzeiger SPL (Stack Pointer Low) auf die Adresse RAMEND eingestellt wird. Ablegen auf den Stapel erfolgt immer so, dass die Stapeladresse niedriger wird, Entnahme vom Stapel erhöht die Adresse entsprechend wieder.

Ein einmal angelegter Stack kann im Programm auch zum zeitweisen Ablegen von Registerinhalten verwendet werden. Mit "PUSH R0" wird der Inhalt des Registers auf den Stapel abgelegt, mit "POP R0" wieder gelesen.

Der Stapel kann ferner dazu verwendet werden, um zeitweilig zu einer anderen Programmadresse zu verzeigen und später wieder zurückzukehren. Mit "RCALL Label" wird die aktuelle Ausführungsadresse auf den Stapel gelegt und dann an die Adresse "Label"verzweigt. Von dort wird mit der Instruktion "RET" wieder an die aufrufende Stelle zurückgekehrt.

Dasselbe passiert bei einem Interrupt. Nur dass hier die Adressablage auf dem Stapel und die Verzweigung vollautomatisch geschieht. Anstelle "RET" wird das angesprungene Programm mit der Instruktion "RETI" beendet, die auch gleich noch das I-Bit im Statusregister auf Eins setzt und die Interruptbearbeitung wieder einschaltet.

6.1.3 Interruptvektoren

Wo springt der Prozessor nun bei einem Interrupt hin? An die ersten Adressen im Flashspeicher. Dort befinden sich die sogenannten Interruptvektoren. Beim ATtiny13 sind das folgende:
#AdresseNameBeschreibung
00000RESETWird beim Reset, beim Anlegen der Betriebsspannung, bei Brown-Out und beim Watchdog-Reset angesprungen
10001INT0Signalflanke am Eingang INT0
20002PCINT0Signalflanke an einem Eingang
30003TIM0_OVFTimer 0 Überlauf
40004EE_RDYSchreibzyklus beim EEPROM abgeschlossen
50005ANA_COMPPolaritätswechsel am Analogvergleicher
60006TIM0_COMPATimer 0 Vergleicher A
70007TIM0_COMPBTimer 0 Vergleicher B
80008WDTWatchdog-Ereignis
90009ADCAD-Wandlung beendet


Die ersten 10 Programmworte in jedem interruptgesteuerten Programm sehen daher so aus:

.CSEG ; Assemblieren in den Flashspeicher (Code Segment)
.ORG 0 ; Adresse auf Null (Reset- und Interruptvektoren beginnen bei Null)
	rjmp Start ; Reset Vektor, Sprung zur Initiierung
	reti ; INT0-Int, nicht aktiv
	reti ; PCINT-Int, nicht aktiv
	reti ; TIM0_OVF, nicht aktiv
	reti ; EE_RDY-Int, nicht aktiv
	reti ; ANA_COMP-Int, nicht aktiv
	reti ; TIM0_COMPA-Int, nicht aktiv
	reti ; TIM0_COMPB-Int, nicht aktiv
	reti ; WDT-Int, nicht aktiv
	reti ; ADC-Int, nicht aktiv
;
; Programmstart beim Reset
;
Start:
	; [Hier beginnt das Programm]

Das "RETI" bei allen nicht verwendeten Vektoren stellt sicher, dass bei einem versehentlich aktivierten Interrupt ein geordneter Rückweg ausgeführt wird.

Hier hat sich bei vielen Programmierern eine Unart entwickelt. Sie setzen die gewünschte Adresse eines verwendeten Vektors mit der Assembler-Direktive „.ORG Vektoradresse“. Daraus macht der Assembler in Verbindung mit dem Programmiergerät dann eine Vektortabelle, die mit lauter NOP-Instruktionen (mit unprogrammierten FFFF) durchsetzt ist. Für die hintersten Vektoren gibt es gar keine Entsprechung, da steht dann schon weiterer Programmcode oder irgendeine andere Interrupt-Service-Routine. Hat man dann einen Interrupt zugelassen, aber vergessen, den Interrupt mit einem Vektor zu versehen, dann wird statt Nichtausführung des fehlenden Vektors (dort stünde bei ordentlichem Programmieren noch ein RETI herum) nach einigen NOP's der nächste programmierte Vektor oder eben das, was auf die Vektortabelle folgt, ausgeführt. Fehlt diesem Code ein RETI, werden fürderhin einfach alle Interrupts blockiert. Das gibt lustige Folgefehler, die zu finden und zu beseitigen einen ziemlichen Aufwand verursachen kann. So kann man sich selbst lustige Beine stellen und das Leben erschweren, ohne groß nachzudenken.

6.1.4 Der Timerüberlauf-Interrupt

Overflow-Int-Enable Um diesen Interrupt zu nutzen, reicht es aus, das Overflow-Interrupt-Enable-Flag TOIE0 im Timer Interrupt Mask Register TIMSK0 zu setzen, z. B. mit "ldi R16 1<<TOIE0" und "out TIMSK0 R16".

Damit der Interrupt angenommen wird, muss natürlich das I-Flag im Statusregister auf 1 gesetzt werden. Damit etwas passiert, muss natürlich noch eine Interrupt-Service-Routine her. Dazu muss in der Interruptvektortabelle in der Zeile mit dem Overflow-Int ein "rjmp ovflw_isr" hin und die Interrupt-Service-Routine ovflw_isr ist zu schreiben. Die kann z. B. zwischen die Vektortabelle und die Start-Routine. So eine typische Routine ist hier dargestellt:

ovflw_isr:
	in R15,SREG ; Statusregister in Register sichern
	dec R17 ; ein Register abwärts zählen
	brne ovflw_isr1 ; springe wenn noch nicht Null
	sbr R18,0b00000001 ; setze Bit 0 im Register R18
	ldi R17,10 ; zaehle ab 10 abwärts
ovflw_isr1:
	out SREG,R15 ; Statusregister wiederherstellen
	reti ; Rückkehr vom Interrupt, I-Flagge setzen

Typisch ist, dass zu Beginn das Statusregister gerettet werden muss. Da viele Instruktionen der Service-Routine die dortigen Flaggen beeinflussen (z. B. die Null-Flagge Z oder die Überlaufflagge C) können, muss dessen Originalzustand zu Beginn gesichert und am Ende wieder hergestellt werden. Schließlich kann die Routine jederzeit zuschlagen, auch wenn woanders mit diesen Flaggen gerade gearbeitet wird. Einhaltung dieser Grundregel "Sichern/Wiederherstellen von SREG in ISRs" ist daher reiner Selbstschutz.

6.1.5 Der Compare-Match-Interrupt

Setzt man eine oder beide Compare-Match-Flaggen OCIE0A oder OCIE0B, dann wird im Anschluss an jede Übereinstimmung mit dem Vergleichswert ein Interrupt ausgelöst. Bei einem Timer im CTC-Modus mit Compare-A tritt nach jedem Rücksetzen des Timers ein Interrupt auf, der zum Vektor TIM0_COMPA führt. Entsprechendes gilt für den Vergleicher B. Selbiges gilt für den PWM-Mode.

6.1.6 Interrupts und Schlafmodus

Im Schlafmodus Idle, den wir bereits verwendet haben, wecken alle Interrupts den Prozessor wieder auf. Damit dieser wieder schlafen gelegt werden kann, muss er in einer Schleife wieder die Instruktion "SLEEP" kriegen. Vor dem Schlafenlegen kann noch geprüft werden, ob eine Interrupt-Service-Routine noch eine Flagge hinterlassen hat, irgendetwas zu tun. Dann wird das eben jetzt erledigt, die Flagge wieder gelöscht und dann erst schlafen gelegt.

Home Top Einführung Hardware Overflow-Int CTC-Int


6.2 Hardware, Bauteile und Aufbau

Zwei LEDs>
Für die Interrupt-Experimente kommt die gleiche Hardware zum Einsatz, wie wir sie schon
in Lektion 2 aufgebaut haben. Jetzt kommt aber eine zweite LED mit Vorwiderstand am Portausgang
OC0B (an Pin 6) hinzu. Diese Bauteile kennen wir schon aus Lektion 2.<br clear=

6.3 Timer mit Overflow-Interrupt

6.3.1 Aufgabenstellung

Die folgende Aufgabenstellung ist zu lösen:

6.3.2 Lösungsschritte

Es ist klar, dass sich diese Aufgabe nicht mit den bisherigen Blinkmechanismen lösen lässt, weil wir für diesen fünften Impuls keine geeigneten Mittel haben und das gleichzeitige Wechseln der Helligkeit der zweiten LED ein arg komplexes Geflecht erfordern würde. Es muss ein Mechanismus her, diesen Aussetzer und die Helligkeitssteuerung zu realisieren. Die Lösung ist ein interruptgesteuerter Zählmechanismus.

So ein Programm sieht grundlegend anders aus als ein Linearprogramm, wie wir es bislang verwendet haben. Weil fast alle Programme Interrupts benutzen, ist es wichtig, diese völlig andere Struktur zu verstehen.

Wir verwenden dazu den Timer-Overflow-Interrupt. Um damit den Sekundentakt hinzukriegen, muss die LED mit zwei Hertz (0,5 Sekunden an + 0,5 Sekunden aus) angesteuert werden. Von 1.200.000 Hz geteilt durch 256 (Timer-Overflow-Takte) = 4.687,5 Hz herab müssten wir einen Interruptzähler bis 4.687,5 / 2 = 2.344 (aufgerundet) zählen lassen. Da das grösser als 255 ist, brauchen wir dafür einen 16-Bit-Zähler.

Weil das Ausblenden der fünften Sekunde etwas aufwändigere Operationen bedingt, verlegen wir diesen Teil nach außerhalb der Interrupt-Service-Routine. Das wäre hier zwar nicht nötig, weil wir nur einen einzigen Interrupt bedienen müssen und bis zum nächsten Interrupt 256 Takte hin sind. Aber wir machen das jetzt hier mal um das Prinzip zu lernen, das bei komplexeren Abläufen ohnehin erforderlich ist.

6.3.3 Programm

Das hier ist eine der denkbaren Lösungen.Hier ist der Quellcode. Die gestrenge Auflistung aller verwendeten Register, Ports und Portbits, die eindeutige Benennung aller verwendeten Konstanten und die vollständige Auflistung aller Interruptvektoren ist übrigens nicht pädagogisch motiviert sondern schlichte Notwehr gegen schlampiges Chaos, das aus leidvoller Erfahrung zu elendig langen Fehlersuch-Sitzungen führt. Das empfiehlt sich nicht nur für Anfänger in Sachen Programmierung, auch Fortgeschrittene haben sich eine solche Struktur längst angeeignet, weil sie dem Ergrauen entgegenwirkt. Dies und die üppige Kommentierung hilft, nach einigen Wochen noch zu verstehen, was der geniale Programmierer sich dabei so gedacht hat.

;
; ***************************************
; * Timer mit Overflow-Interrupt        *
; * (C)2016 by www.gsc-elektronik.net   *
; ***************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; ---------- Register -------------------
; frei: R0 .. R14
.def rSreg = R15 ; SREG-Zwischenspeicher
.def rmp = R16 ; Vielzweckregister
.def rimp = R17 ; Vielzweckregister Interrupts
.def rFlag = R18 ; Flaggenregister
	.equ bPol = 0 ; Flagge Polaritaetswechsel
.def rZaehl= R19 ; Blinkzaehler
.def rPwm = R20 ; PWM-Zaehler
; frei: R20 .. R23
.def rCntL = R24 ; Zaehler, LSB
.def rCntH = R25 ; dto., MSB
;frei: R26 .. R31
;
; ---------- Ports, Portbits ------------
.equ pLedOut = PORTB ; LED-Ausgabeport
.equ bLedOut = PORTB0
.equ pLedDdr = DDRB ; LED-Richtungsport
.equ bLedDdr = DDB0
.equ pLedIn  = PINB ; LED-Eingabeport
.equ bLedIn  = PINB0
;
.equ bPwmOut = PORTB1 ; Ausgabeport PWM
.equ bPwmDdr = DDB1 ; Richtungsport PWM
;
; ---------- Timing ---------------------
.equ cClock  = 1200000 ; Takt
.equ cPolwechsel = 2 ; Taktfrequenz Led An/Aus
.equ cPresc  = 1 ; Vorteiler Timer
.equ cCount  = cClock / 256 / cPresc / cPolwechsel + 1
.equ cBlink  = 5 ; Blinkzaehler
;
; ---------- Reset- und Interrupt-Vektoren -------
.CSEG ; Programmcode
.ORG 0 ; Reset- und Vektorenadresse
	rjmp Start ; Reset Vektor, Sprung zur Initiierung
	reti ; INT0-Int, nicht aktiv
	reti ; PCINT-Int, nicht aktiv
	rjmp Tc0IsrO ; TIM0_OVF, aktiv
	reti ; EE_RDY-Int, nicht aktiv
	reti ; ANA_COMP-Int, nicht aktiv
	reti ; TIM0_COMPA-Int, nicht aktiv
	reti ; TIM0_COMPB-Int, nicht aktiv
	reti ; WDT-Int, nicht aktiv
	reti ; ADC-Int, nicht aktiv
;
; ---------- Interrupt-Service-Routinen ----------
; Timer 0 Overflow:
;   Der Timer laeuft mit einem Vorteiler von 1 und
;   ruft bei jedem Ueberlauf von 255 auf 0 diese
;   Interrupt-Service-Routine auf (256 / 1.200.000 =
;   213 µs, Frequenz = 1.200.000 / 256 = 4,6875
;   kHz).
;   In dieser wird der 16-Bit-Zaehler rCntH:rCntL,
;   der zu Beginn und beim Erreichen von Null auf
;   1.200.000 / 256 / 2 = 2.343 gesetzt wird, um
;   eins vermindert. Ist der Zaehler gleich Null,
;   wird die Flagge rPol im Flaggenregister rFlag
;   gesetzt und der Zaehler neu gestartet. Das ist
;   alle 2.343 * 256 / 1.200.000 = 0,499984 s der
;   Fall.
;   Ausserdem wird das LSB des Zaehlers daraufhin
;   ueberprueft, ob die unteren fuenf Bits Null
;   sind (ist alle 32 Timer-Interrupt-Takte =
;   213 us * 32 = 68,3 ms der Fall). Wenn das der
;   Fall ist, wird der Vergleichswert B (rPwm) um
;   Eins vermindert und die Helligkeit der LED
;   nimmt zu (kürzere Einsphase mit LED aus 
;   ist längere Nullphase der LED mit LED an).
;   War das 256 mal der Fall (= 68,3 ms * 256 =
;   17,5 s) startet der Helligkeitswert wieder mit
;   höchster Helligkeit (1 Timer-Takt lang
;   LED aus, 255 Timer-Takte lang LED an).
;
Tc0IsrO: ; Timer 0 Overflow ISR
	in rSreg,SREG ; Statusregister sichern
	sbiw rCntL,1 ; Zaehler um Eins verringern
	brne Tc0IsrO1 ; springe wenn nicht Null
	sbr rFlag,1<<bPol ; Flagge Polaritaetswechsel setzen
	ldi rCntH,HIGH(cCount) ; Zaehler neu starten
	ldi rCntL,LOW(cCount)
Tc0IsrO1:
	mov rimp,rCntL ; Low Byte Zaehler kopieren
	andi rimp,0b00011111 ; die unteren fuenf Bit isolieren
	brne Tc0IsrO2 ; springe wenn nicht Null
	dec rPwm ; PWM-Wert abwaerts
	out OCR0B,rPwm ; in Vergleichsregister B
Tc0IsrO2:
	out SREG,rSreg ; Statusregister wieder herstellen
	reti ; Ende ISR, I-Bit setzen
;
; ---------- Initiieren und Programmschleife -----
Start:
	; Stapel initiieren
	ldi rmp,LOW(RAMEND) ; Stapelzeiger auf SRAM-Ende
	out SPL,rmp ; Stapelzeiger setzen
	; LED-Ausgang aktivieren
	ldi rmp,(1<<bLedDdr)|(1<<bPwmDdr) ; LED-Ausgaenge an, Treiber an
	out pLedDdr,rmp ; in Port-Richtungsregister
	; Zaehler initiieren
	ldi rZaehl,cBlink ; Blinkzaehler initiieren
	ldi rCntH,HIGH(cCount) ; Zaehler auf Startwert
	ldi rCntL,LOW(cCount)
	; Vergleichsregister initiieren
	ldi rmp,0xFF ; Compare A auf 255
	out OCR0A,rmp ; in Vergleichsregister A
	clr rPwm ; Startwert PWM
	out OCR0B,rPwm ; in Vergleichsregister B
	; Zaehler 0 als starten mit Overflow-Interrupt
	ldi rmp,(1<<COM0B1)|(1<<WGM01)|(1<<WGM00) ; Fast PWM, COM0B
	out TCCR0A,rmp ; in Timer Kontrollregister A
	ldi rmp,1<<CS00 ; Vorteiler durch 1
	out TCCR0B,rmp ; in Timer Kontrollregister B
	ldi rmp,1<<TOIE0 ; Overflow-Interrupt
	out TIMSK0,rmp ; in Timer-Interrupt-Maske
	; Schlafmodus
	ldi rmp,1<<SE ; Sleep-Enable, Mode Idle
	out MCUCR,rmp ; in Universal-Kontrollregister
	; Interrupts ermoeglichen
	sei ; setze Interrupt-Flagge im Statusregister
; Programmschleife
Schleife:
	sleep ; schlafen legen
	nop ; nach Aufwachen
	sbrc rFlag,bPol ; ueberspringe naechste Instruktion wenn Flagge Null
	rcall Polaritaet ; Flagge bearbeiten: Polaritaetswechsel
	rjmp Schleife ; wieder schlafen legen
;
; --------- Polaritaet der LED wechseln ----------
Polaritaet:
	cbr rFlag,1<<bPol ; Flagge loeschen
	sbis pLedOut,bLedOut ; ueberspringe naechste Instruktion wenn Led An
	rjmp Polaritaet3 ;  Led ist an, springe zu LED aus
	; Led ist Aus
	cpi rZaehl,0 ; ist Blinkzaehler Null?
	breq Polaritaet1 ; ja, ist Null, starte Zaehler neu
	; Led Aus, Zaehler nicht Null
	dec rZaehl ; nein, vermindere Blinkzaehler
	brne Polaritaet2 ; Led einschalten
	ret ; kein Polaritaetswechsel
Polaritaet1: ; Neustart Zaehler
	ldi rZaehl,cBlink ; starte Zaehler neu
	ret ; kein Polaritaetswechsel
Polaritaet2: ; Led einschalten
	cbi pLedOut,bLedOut ; Led an
	ret
Polaritaet3: ; Led ausschalten
	sbi pLedOut,bLedOut ; Led aus
	ret
;
; Ende Quellcode
;

Neu sind die Instruktionen

6.3.4 Das Ergebnis

Led-Kombination So leuchten zwei LEDs in unterschiedlichem Modus: die eine abrupt mit fehlendem fünftem Impuls, die andere mit sanft veränderter Helligkeit. Und das alles mit minimiertem Strombedarf, weil der Prozessor überwiegend im Schlafmodus herumschnarcht und nur bei Bedarf geweckt wird und danach in der Regel wieder schlafen gelegt wird.

6.3.5 Programmgliederung

Die erkennbare Gliederung des Programmes in
  1. Registerdefinitionen, Portfestlegungen und Konstanten zu Beginn,
  2. Reset- und Interruptvektoren,
  3. Interrupt-Service-Routinen,
  4. Hauptprogramm-Init mit Hardwarekonfiguration und Startwerten,
  5. Schleifenbearbeitung, und
  6. Bearbeitungsroutinen außerhalb von Interrupt-Service-Routinen
macht bei interruptgesteuerten Programmen immer Sinn, um den Überblick auch bei komplizierteren Abläufen zu behalten. Es hilft immens bei der Fehlersuche, weil man potenzielle Fehlerverursacher schneller findet.

6.3.6 Simulation der Vorgänge

Die Simulation wird mit avr_sim unter den folgenden Rahmenbedingungen durchgeführt: Dies hier sind die Einstellungen nach der Initiierung der Hardware:

TC0-Überlauf-Interrupt enabled Der Timer TC0 ist im Fast-PWM-Modus, sein TOP-Wert und sein Vergleichswert A ist auf 255 gesetzt, der PWM-Vergleichswert im Compare-B-Portregister ist Null. Der Vorteiler ist auf 1 gesetzt und sein Überlauf-Interrupt-Enable-Bit ist gesetzt. Beim Erreichen des Compare-B-Wertes wird die LED am Ausgang PB1 auf Null geschaltet (PB1 clear), ist also an. Zu Beginn jedes PWM-Zyklusses ist PB0 gleich Eins, die LED also ausgeschaltet. Je größer also der Vergleichswert, desto schwächer die LED-Helligkeit (invertierende PWM).

Port B nach dem Init Nach dem Init sind im Port B beide Richtungsbits gesetzt, um die beiden LEDs anzusteuern. Die Portbits PORTB0 und PORTB1 sind Null, beide LEDs sind an.

Zeitbedarf für Init Die Initiierung dauerte 18,33 µs, hat den Stapelzeiger ("stack pointer") auf das Ende des SRAMs im ATtiny13 gesetzt und den 16-Bit-Zähler R25:R24 auf 2.344 (=hexadezimal 0928) gestellt. Der LED-Abwärtszähler in R19 wurde auf 5 gesetzt und das Interrupt-Enable-Bit I im Statusregister SREG wurde gesetzt. Damit kann der interrupt-gesteuerte Zirkus so richtig beginnen.

Erster Überlauf, Interrupt angefordert Der erste Überlauf des Timers ist aufgetreten, der Timer startet neu und hat seine Überlauf-Flagge gesetzt. Mit der Ausführung der nächsten Instruktion führt der Controller den Interrupt aus und verzweigt zur Interrupt-Behandlungsroutine.

Zeit bis zum ersten Überlauf Port nach dem ersten Überlauf Der erste Überlauf passiert nach 208,3 µs, zusammen mit den Interrupt-Enable-Instruktionen, dem Setzen der I-Flagge und der SLEEP-Instruktion 213 µs.

Beim Überlauf wurde des Portbit PB1 gesetzt, die LED ausgeschaltet und der PWM-Zyklus neu begonnen.

Compare Match B Mit der nächsten Instruktion wird schon der Compare Match B ausgeführt, weil sowohl TCNT als auch Compare B bei der letzten Instruktion Null waren. Die LED war aus für einen Zählzyklus und wird jetzt eingeschaltet. Bitte erinnern: solch ein OC-Ausgang kann nicht vollkommen auf Null gehen, ein einziger Zyklus ist die LED immer aus, auch wenn Compare B auf Null steht.

TC0 Interrupt-Ausführung Vollkommen unabhängig von den Vorgängen am Portausgang wird jetzt der Timer-Interrupt ausgeführt. Der Controller hat die Adresse des Programmzählers (der auf dem SLEEP steht, auf dem Stapel abgelegt (zwei Bytes) und ist zur Vektoradresse des TC0-Überlaufes verzweigt.

Simulation Int + 1 Durch die Interruptausführung wurde der Stapelzeiger um zwei erniedrigt und das I-Bit im Statusregister SREG gelöscht.

Zähler vermindern Innerhalb der Interrupt-Behandlungsroutine wurde der 16-Bit-Zähler in R25:R24 um Eins erniedrigt. Weil er noch nicht Null ist (siehe die nicht gesetzte Z-Flagge im SREG), wird auch die bPol-Flagge nicht gesetzt, der Polaritätswechsel bei der ersten LED an PB0 erfolgt daher noch nicht.

TC0 Interrupt nach RETI Simulation nach RETI Mit der RETI-Instruktion wird der Interrupt beendet. Das Interrupt-Request-Bit des Timers wurde schon bei der Ausführung des Interrupts automatisch zurückgesetzt, das Overflow-Enable-Bit des Timers bleibt aber weiter gesetzt. Der Stapelzeiger hat wieder seinen alten Wert, denn RETI holt die beiden Bytes wieder vom Stapel und verzweigt wieder zur SLEEP-Instruktion, wo der Interrupt den Programmablauf unterbrochen hat. Die I-Flagge im SREG ist wieder gesetzt, auch das hat RETI erledigt.

Nun versinkt der Controller aber keineswegs wieder in den Schlaf, weil er vom Timer-Interrupt aufgeweckt wurde. Mit der nächsten Instruktion nach dem SLEEP wird der Programmablauf fortgesetzt und mit sbrc rFlag,bPol und rcall Polaritaet die Polaritäsflagge abgefragt. Die ist 2.343 mal nicht gesetzt und der Controller überspringt die RCALL-Instruktion und geht mit rjmp Schleife wieder in den Schlaf. Beim 2.344-sten Mal nicht: dann wird mit RCALL zum nachfolgenden Code verzweigt und ein Polaritätswechsel bei der LED an PB0 durchgeführt.

TC0 Compare-Wert wechselt Eine besondere Bedingung ergibt sich, wenn der Compare-Wert für Compare B in der Interruptbehandlungs-Routine geändert wird. Der zum Einsatz kommende Compare-Wert B wird beim Fast-PWM-Modus erst dann in das Vergleichsregister übernommen, wenn der ablaufende PWM-Zyklus sein Ende erreicht hat. Bis das der Fall ist, kommt noch der alte Vergleichswert zum Einsatz (Gelbfärbung des Compare-B-Werts signalisiert das). Durch diesen Mechanismus wird sichergestellt, dass kein Compare-Ereignis verpasst wird, wenn der alte Wert über TCNT, der neue Wert aber unter TCNT liegt. Auch kann der Fall nicht eintreten, dass in einem Zyklus zwei Compare-Matches eintreten (wenn der alte Wert niedriger liegt als TCNT, der neue Wert aber über TCNT). Alle solche Katastrophen werden durch das zeitweise Zwischenspeichern des neuen Werts vermieden.

Ende des ersten PWM-Zyklusses Das ist die Zeit wenn der erste Zähl-Zyklus in R25:R24 beendet ist: Die Zählzeit für das Blinken der LED an PB0 ist ungefähr zutreffend. Es wäre genauer, wenn wir am immer gleichen Breakpoint anhalten würden und die Stopuhr definiert rücksetzen würden (mit der Maustaste in die Stopuhr-Zeile klicken).

Ende des zweiten PWM-Zyklus Hier ist das Ende des zweiten PWM-Zyklusses erreicht. Etwa eine Sekunde ist seither vergangen bis die LED wieder eingeschaltet wird.

6.3.7 Vor- und Nachteile

Der Vorteil bei dieser Lösung ist die hohe Flexibiltät (leichte Modifizierbarkeit). So kann die Blinkfrequenz, der zu überspringende Blinkimpuls oder der LED-Port einfach durch Änderung von Konstanten im Kopf auf anderen Bedarf umgestellt werden.

Der Nachteil der Lösung ist, dass die Blinkdauer nur ungefähr stimmt. Das ist darauf zurückzuführen, dass das Teilen durch 256 nicht zu passenden Ganzzahlen führt. Diesen Nachteil behebt das nächste Lösung.

Home Top Einführung Hardware Overflow-Int CTC-Int


6.4 Timer mit CTC-Interrupt

Damit es exakt zugeht, z. B. bei einer Uhr, ist eine exaktere Lösung zwingend. Die geht wie hier gezeigt.

6.4.1 CTC-Auswahl

Um exakte Teilerverhältnisse zu erhalten, muss die Kombination von Taktfrequenz, Vorteiler, CTC-Wert und Software-Zähler exakt den Zielwert ergeben. In der Tabelle sind für alle Taktfrequenzen des ATtiny13 und für alle Vorteiler diejenigen CTC-Teiler angegeben, die sich mit einem 8- oder 16-Bit-Zähler realisieren lassen, um 1 Hz zu erhalten. Die Verhältnisse sind farblich gekennzeichnet.
Farben:16-Bit-Zähler 8-Bit-ZählerCTC=256 CTC=256/8-Bit-Zähler

Daraus ergeben sich folgende Ergebnisse (nur CTC-Werte > 100 sind aufgelistet).
Takt (Hz)Prescaler Ganzzahlige CTC-Teiler
9.600.0001256 (37500) 250 (38400)240 (40000) 200 (48000)192 (50000) 160 (60000)150 (64000)
8250 (4800)240 (5000) 200 (6000)192 (6250) 160 (7500)150 (8000) 128 (9375)
64250 (600)240 (625) 200 (750)150 (1000)
256250 (150)150 (250)
1024
4.800.0001256 (18750) 250 (19200)240 (20000) 200 (24000)192 (25000) 160 (30000)150 (32000) 128 (37500)
8250 (2400)240 (2500) 200 (3000)192 (3125) 160 (3750)150 (4000)
64250 (300)200 (375) 150 (500)
256250 (75)150 (125)
1024
1.200.0001250 (4800) 240 (5000)200 (6000) 192 (6250)160 (7500) 150 (8000)128 (9375)
8250 (600)240 (625) 200 (750)150 (1000)
64250 (75)150 (125)
256
1024
128.0001256 (500) 250 (512)200 (640) 160 (800)128 (1000)
8250 (64)200 (80) 160 (100)128 (125)
64250 (8)200 (10)
256250 (2)
1024
Für 1,2 MHz Takt bietet sich eine CTC-Teilung durch 250 (125 für 2 Hz) bei einem Vorteiler von 64 an. Da die zweite LED ein PWM-Signal erzeugen muss, muss der Timer im PWM-Modus betrieben werden.

6.4.2 Programm

Für das Zählen reicht nun ein 8-Bit-Register. Hier ist der Quellcode.

;
; ***************************************
; * Timer mit COMP-A-Interrupt          *
; * (C)2016 by www.gsc-elektronik.net   *
; ***************************************
;
.NOLIST
.INCLUDE "tn13def.inc"
.LIST
;
; ---------- Register -------------------
; frei: R0 .. R14
.def rSreg = R15 ; SREG-Zwischenspeicher
.def rmp = R16 ; Vielzweckregister
.def rimp = R17 ; Vielzweckregister bei Interrupts
.def rFlag = R18 ; Flaggenregister
	.equ bPol = 0 ; Flagge Polaritaetswechsel
.def rZaehl= R19 ; Blinkzaehler
.def rCtcInt = R20 ; CTC-Interrupt-Zaehler
.def rPwm = R21 ; PWM-Zaehler
; frei: R22 .. R31
;
; ---------- Ports, Portbits ------------
.equ pLedOut = PORTB ; LED-Ausgabeport
.equ bLedOut = PORTB0
.equ pLedDdr = DDRB ; LED-Richtungsport
.equ bLedDdr = DDB0
.equ pLedIn  = PINB ; LED-Eingabeport
.equ bLedIn  = PINB0
;
.equ bPwmOut = PORTB1 ; zweite LED im PwmMode
.equ bPwmDdr = DDB1
;
; ---------- Timing ---------------------
.equ cClock  = 1200000 ; Takt
.equ cPolwechsel = 2 ; Taktfrequenz Led An/Aus
.equ cPresc  = 64 ; Vorteiler Timer
.equ cCtcInt = 125 - 1 ; CTC-Int-Zaehler
.equ cCtcCnt = cClock / cPresc / cPolwechsel / (cCtcInt +1)
.equ cBlink  = 5 ; Blinkzaehler
;
; ---------- Reset- und Interrupt-Vektoren -------
.CSEG ; Programmcode
.ORG 0 ; Reset- und Vektorenadresse
	rjmp Start ; Reset Vektor, Sprung zur Initiierung
	reti ; INT0-Int, nicht aktiv
	reti ; PCINT-Int, nicht aktiv
	reti ; TIM0_OVF, nicht aktiv
	reti ; EE_RDY-Int, nicht aktiv
	reti ; ANA_COMP-Int, nicht aktiv
	rjmp Tc0IsrA ; TIM0_COMPA-Int, nicht aktiv
	reti ; TIM0_COMPB-Int, nicht aktiv
	reti ; WDT-Int, nicht aktiv
	reti ; ADC-Int, nicht aktiv
;
; ---------- Interrupt-Service-Routinen ----------
Tc0IsrA: ; Timer 0 Compare A Interrupt
	in rSreg,SREG ; Statusregister sichern
	dec rPwm ; PWM-Zaehler erniedrigen
	brne Tc0IsrA1 ; springen wenn nicht Null
	ldi rPwm,cCtcInt ; Neustart PWM-Zaehler
Tc0IsrA1:
	out OCR0B,rPwm ; PWM aktualisieren
	dec rCtcInt ; Zaehler um Eins verringern
	brne Tc0IsrA2 ; springe wenn nicht Null
	sbr rFlag,1<<bPol ; Flagge Polaritaetswechsel setzen
	ldi rCtcInt,cCtcCnt ; Zaehler neu starten
Tc0IsrA2:
	out SREG,rSreg ; Statusregister wieder herstellen
	reti ; Ende ISR, I-Bit setzen
;
; ---------- Initiieren und Programmschleife -----
Start:
	; Stapel initiieren
	ldi rmp,LOW(RAMEND) ; Stapelzeiger auf SRAM-Ende
	out SPL,rmp ; Stapelzeiger setzen
	; LED-Ausgang aktivieren
	ldi rmp,(1<<bPwmDdr)|(1<<bLedDdr) ; LED-Ausgaenge, Treiber an
	out pLedDdr,rmp ; in Richtungsregister
	; Zaehler initiieren
	ldi rZaehl,cBlink ; Blinkzaehler initiieren
	ldi rCtcInt,cCtcCnt ; Zaehlerstartwert
	ldi rPwm,cCtcInt ; PWM initiieren
	; Compare A-Wert fuer CTC setzen
	ldi rmp,cCtcInt ; CTC-Teiler durch 125
	out OCR0A,rmp ; in Vergleicher A
	; Zaehler 0 normal starten mit Overflow-Interrupt
	ldi rmp,(1<<COM0B1)|(1<<WGM01)|(1<<WGM00) ; PWM am Port OC0B, Fast PWM
	out TCCR0A,rmp ; in Timer Kontrollregister A
	ldi rmp,(1<<WGM02)|(1<<CS01)|(1<<CS00) ; Fast PWM, Vorteiler durch 64
	out TCCR0B,rmp ; in Timer Kontrollregister B
	ldi rmp,1<<OCIE0A ; Compare A-Interrupt
	out TIMSK0,rmp ; in Timer-Interrupt-Maske
	; Schlafmodus
	ldi rmp,1<<SE ; Sleep-Enable, Mode Idle
	out MCUCR,rmp ; in Universal-Kontrollregister
	; Interrupts ermoeglichen
	sei ; setze Interrupt-Flagge im Statusregister
; Programmschleife
Schleife:
	sleep ; schlafen legen
	nop ; nach Aufwachen
	sbrc rFlag,bPol ; ueberspringe naechste Instruktion wenn Flagge Null
	rcall Polaritaet ; Flagge bearbeiten: Polaritaetswechsel
	rjmp Schleife ; wieder schlafen legen
;
; --------- Polaritaet der LED wechseln ----------
Polaritaet:
	cbr rFlag,1<<bPol ; Flagge loeschen
	sbis pLedOut,bLedOut ; ueberspringe naechste Instruktion wenn Led An
	rjmp Polaritaet3 ;  Led ist an, springe zu LED aus
	; Led ist Aus
	cpi rZaehl,0 ; ist Blinkzaehler Null?
	breq Polaritaet1 ; ja, ist Null, starte Zaehler neu
	; Led Aus, Zaehler nicht Null
	dec rZaehl ; nein, vermindere Blinkzaehler
	brne Polaritaet2 ; Led einschalten
	ret ; kein Polaritaetswechsel
Polaritaet1: ; Neustart Zaehler
	ldi rZaehl,cBlink ; starte Zaehler neu
	ret ; kein Polaritaetswechsel
Polaritaet2: ; Led einschalten
	cbi pLedOut,bLedOut ; Led an
	ret
Polaritaet3: ; Led ausschalten
	sbi pLedOut,bLedOut ; Led aus
	ret
;
; Ende Quellcode
;

Natürlich sehen wir den Taktunterschied bei der LED gegenüber der vorherigen Lösung nicht, weil das menschliche Auge unempfindlich ist für solche minimalen Unterschiede. Wir wissen aber, dass das jetzt genau ist, weil wir es so programmiert haben.

6.4.3 Simulation

TC0 CTC nach Init Port B nach init Das sind die Konfigurationen des Timers und des Ports nach der Initiierung. Der Timer ist im Fast-PWM-Modus, mit einem Vorteiler von 64. Sein TOP-Wert wird vom Compare-Register A bestimmt (124, Rücksetzen bei (124 + 1), Dauer = 125 * 64 / 1.200.000 = 6,667 ms). Der Port B treibt die LED-Ausgänge über die PORTB-Bits PORTB0 und PORTB1. PORTB1 wird wieder vom Timer gesteuert (Beginn PWM-Zyklus: LED aus, Compare Match B: LED an). Beim Compare Match A wird der TC0 Compare Match Int ausgelöst.

Der erste CTC-Int TC0 vom Timer Port B beim ersten CTC-Int Der Timer hat den CTC-Wert 124 erreicht und mit dem nächsten Timer-Tick das Zählregister TCNT rückgesetzt und führt den Compare-Match-A-Interrupt aus. Der Compare-Match-B-Wert wurde auf 123 gesetzt, einen Puls vor dem Compare-Match A. Der Portausgang PB1 ist auf Eins gesetzt, die LED ist aus.

Zeit bis zum ersten CTC-Int Seit dem Eintritt in den Schlafmodus sind exakt 6,67 ms vergangen, die Zeit stimmt mit der Berechnung überein.

Zeit bis zum ersten Polaritätswechsel Das ist das Ende des ersten Zyklus: die Polaritätsflagge wird gesetzt und eine halbe Sekunde ist vergangen.

Exaktes timing mit dem Timer im CTC-Modus kann für fast beliebige Zeiten eingestellt werden. Dabei spielt es keine Rolle, was der Prozessor sonst noch tun mag und wie lange das dauert. In diesem Fall wurden zwei Aufgaben kombiniert (Schalten der Sekunden-LED, Helligkeitsregelung bei der zweiten LED), ohne dass sich beide Aufgaben irgendwie beissen. Viele andere Aufgaben können in diesen Ablauf eingehängt werden. Und das alles mit nur einem einzigen Timer.

Home Top Einführung Hardware Overflow-Int CTC-Int


©2016-2018 by http://www.gsc-elektronic.net