Forum: Mikrocontroller und Digitale Elektronik Timer-Interrupt mit Schluckauf (ATmega328P)


von Jonas P. (jox)


Lesenswert?

Hallo,

ich frage mich gerade ob, wenn man einen intern (!) getakteten Timer mit 
Interrupt verwendet, irgendwelche Pins dadurch beeinflusst werden bzw. 
irgendwelche Pins den Timer/Interrupt beeinflussen. Was ich erlebe, ist 
genau das. Einige Pins sind ja mit den Timer/Countern verbunden.

Ein konkretes Beispiel: wenn ich den Timer 2 im CTC und eine ISR auf 
TIMER2_COMPA_vect definiere, habe ich einen sauber laufenden Interrupt. 
Ein in der ISR getoggleter Pin (PD3) erzeugt im Oszilloskop ein stabiles 
Rechtecksignal.

PD3 im Oszi:
1
 _   _   _   _
2
| |_| |_| |_| |_

Wenn ich nun im main loop das Bit PD5 (welches als Output gesetzt ist) 
regelmäßig ändere, fängt das Rechtecksignal an zu zucken. Genauer 
betrachtet sieht man, dass anscheinend ISR-Aufrufe verschluckt werden.

PD3 im Oszi:
1
 ___   _     ___   _
2
|   |_| |___|   |_| |_
oder auch
1
 ___     ___     ___  
2
|   |___|   |___|   |___

Das Output-Pin (D5) funktioniert wie erwartet.

Die zusätzliche Funktionen für PD5 sind "T1/OC0B/PCINT21". PD3 hat 
"INT1/OC2B/PCINT19"

Hier die Timer-Initialisierung:
1
TCCR2A = (1<<WGM21); // CTC
2
TCCR2B = (1<<CS22) | (1<<CS21) | (1<<CS20); // /1024
3
OCR2A  = (F_CPU/1024)/100; // ~10 ms
4
TIMSK2  = (1 << OCIE2A);

Gibt es dafür eine schnelle Erklärung? Falls nicht kann ich gerne mehr 
Infos bzw. Sourcecode liefern.

Das ganze passiert im ATmega328 auf einem Arduino Duemilanove.

Danke und Gruß
Jonas

von Jonas P. (jox)


Lesenswert?

Vielleicht sollte ich noch erwähnen, dass das beschriebene Verhalten 
nicht bei allen Pins auftritt. Ändere ich z.B. PD6 oder auch PB0 im main 
loop, tritt das Problem nicht auf. Bei  PD5 oder PD7 schon.

Jonas

von Oliver J. (skriptkiddy)


Lesenswert?

Kompletten Quelltext bitte. Ich hab meine Glaskugel schon im Schubfach.

von Peter D. (pdiener) Benutzerseite


Lesenswert?

Das ist offenbar ein read-modify-write Problem.
Das passiert, wenn die Pins nicht mit setbit oder clearbit geschaltet 
werden, sondern der Port in ein Pufferrgister eingelesen wird, ein Bit 
im Pufferregister geändert wird (z.B. getoggelt) und der Registerwert 
dann wieder auf den Port geschrieben wird.

Wenn der Interrupt eine read-modify-write Anweisung der Hauptschleife 
unterbricht und den Portwert selbst ändert, weiß die Hauptschleife davon 
nichts und schreibt einen veralteten Portwert aus dem Pufferegister auf 
den Port raus. Die Änderungen, die der Interrupt am Port vorgenommen 
hat, sind damit nach sehr kurzer Zeit überschrieben.

Schau dir mal das Assemblerlistung von deinem Programm an, ich bin mir 
fast zu 100% sicher, dass es genau so ein Problem ist.

Stichwort "atomarer Registerzugriff"!

Damit das funktioniert, muss im Hauptprogramm bei jedem nicht atomaren 
Zugriff auf Register, die der Interrupt verändern darf, der 
entsprechende Interrupt für die Zeit des Zugriffs gesperrt werden.

Grüße,

Peter

von Jonas P. (jox)


Lesenswert?

Oh ja, mit cli() vor und sei() nach dem ändern der Bits in der 
Hauptschleife bleibts sauber!

Dann fehlten nicht wie ich vermutete Interrupts, sondern das 
"Rechteck-Bit" lieferte ein falsches Signal ans Oszilloskop.

Vielen Dank! Ich war die ganze Zeit auf der falschen Fährte...

Werd mir das Assemblerlisting noch ansehen.

Gruß
Jonas

von Jonas P. (jox)


Lesenswert?

Hier der Vollständigkeit halber noch ein reduzierter Quelltext zum 
reproduzieren:
1
#include <avr/io.h>
2
#include <avr/interrupt.h>
3
#include <util/delay.h>
4
5
ISR( TIMER2_COMPA_vect ) {
6
  PORTD ^= (1<<PD3);
7
}
8
9
int main(void) {
10
11
  DDRD |= (1<<PD5) | (1<<PD3);
12
13
  TCCR2A = (1<<WGM21); // CTC
14
  TCCR2B = (1<<CS22) | (1<<CS21) | (1<<CS20); // /1024
15
  OCR2A  = (F_CPU/1024)/100; // ~10 ms
16
  TIMSK2  = (1 << OCIE2A);
17
18
  sei();
19
20
  for (;;) {
21
    // ohne cli() und sei() = Problem     
22
    // cli();
23
    PORTD |= (0<<PD5);
24
    // sei();
25
  }
26
27
  return 0;
28
}

von Jonas P. (jox)


Lesenswert?

Ist mir inzwischen wie Schuppen von den Augen gefallen.

Die Zeile:
1
PORTD |= (0<<PD5);

bedeutet ja:
1
PORTD = PORTD | (0<<PD5)

Also read-modify-write, wie du erkannt hast, Peter. Und der Interrupt 
funkte regelmäßig irgendwo ins 'modify' rein. Beim 'write' wurden dann 
die Änderungen des Interrups zunichte gemacht.

Das auch nur, weil es auf dem gleichen Port passierte, wenn auch 
(vermeintlich) verschiedene Bits betraf.

Danke nochmal!

von Andreas F. (aferber)


Lesenswert?

Bei neueren AVRs (ATmega328P ist neu genug) gibt es noch eine andere 
Möglichkeit, wenn der Pin 5 nicht explizit auf 0 bzw. 1 gesetzt, sondern 
zwischen den beiden Zuständen umgeschaltet werden soll:
1
PIND = 1<<PD5;

Das gilt natürlich nur für den in main() gesetzten Pin, im Interrupt 
würde das nicht helfen.

Beachte: es ist volle Absicht, dass da kein '|' drinsteht!

Andreas

von Falk B. (falk)


Lesenswert?

Klassisches Problem von nichtatomarem Zugriff, siehe Interrupt.

MFG
Falk

von Spess53 (Gast)


Lesenswert?

Hi

Nur am Rande bemerkt:

>PORTD |= (0<<PD5);

diese Zeile ist sinnlos.

MfG Spess

von Karl H. (kbuchegg)


Lesenswert?

Jonas P. schrieb:
> Ist mir inzwischen wie Schuppen von den Augen gefallen.

>
> Die Zeile:
>
>
1
> PORTD |= (0<<PD5);
2
>


Die Zeile sollte eigentlich
1
  PORTD |= (1<<PD5);

heissen. Mit einer 0 ist das recht sinnfrei.

Macht man es aber richtig, so erkennt der Compiler die Absicht und der 
Optimizer ersetzt das komplette Konstrukt durch einen Einzelbit-Setz 
Befehl, wodurch dann auch wieder alles Paletti ist. Das ist dann von 
Haus aus atomar.

Das ist dann wohl auch einer der Grund, warum dieses Problem in der 
Praxis nicht öfter auftritt.

Also:
korrekt schreiben
Optimizer aufdrehen

von Jonas P. (jox)


Lesenswert?

Vielen Dank für die Kommentare.

>> PIND = 1<<PD5;

@Andreas: Ja stimmt, das habe ich auch im Datenblatt gelesen: "However, 
writing a logic one to a bit in the PINx Register, will result in a 
toggle in the corresponding bit in the Data Register.". Kann ganz 
nützlich sein.

>> diese Zeile ist sinnlos.

@Spess53: Ihr habt natürlich Recht. Lustigerweise trat das Problem nur 
mit "PORTD |= (0<<PD5);" auf. Bei "PORTD |= (1<<PD5);" nicht. Wie gesagt 
habe ich den Code aufs nötige reduziert um das Phänomoen zu 
reproduzieren. Bei "PORTD |= (1<<PD5);" tritt nehme ich an durch die 
Logik kein 'write' mehr auf. Wenn das Outputbit 5 die ganze Zeit den 
Wert 1 hat resultiert die Operation in "1 | 1" und bricht vor dem Prüfen 
des zweiten Operanden ab (Fall schon erfüllt, keine Modifikation nötig). 
Oder so ähnlich.

Eigentlich hatte ich ein funktionierendes Programm und wollte nur die 
Interruptfrequenz prüfen (durch toggeln eines Pins in der ISR). Dann war 
ich irritiert als das Ergebnis (scheinbar) so instabil war. Jetzt ist 
mir klar, dass ich mir und meinem Programm damit ein Bein gestellt habe.

Gruß
Jonas

von Jonas P. (jox)


Lesenswert?

>> ersetzt das komplette Konstrukt durch einen Einzelbit-Setz Befehl

Ja, oder so...

von Jonas P. (jox)


Lesenswert?

Hab mich verzettelt. "@Spess53: Ihr habt natürlich Recht." sollte 
"@Spess53  und Karl Heinz: Ihr habt natürlich Recht." werden. Keine 
Hoheitsanrede... ;-)

von Karl H. (kbuchegg)


Lesenswert?

Jonas P. schrieb:

> reproduzieren. Bei "PORTD |= (1<<PD5);" tritt nehme ich an durch die
> Logik kein 'write' mehr auf. Wenn das Outputbit 5 die ganze Zeit den
> Wert 1 hat resultiert die Operation in "1 | 1" und bricht vor dem Prüfen
> des zweiten Operanden ab (Fall schon erfüllt, keine Modifikation nötig).

Das wiedrrum kann nicht sein.
PORTD ist eine volatile Einheit. Und als solche muss der Compiler jeden 
Zugriff darauf auch durchführen.

> Oder so ähnlich.

Es ist schon so, dass dich hier der Optimizer rettet, indem er diesen 
Zugriff durch die Wahl der Assembler Operation atomar macht.

Was der allerdings macht bei


  PORTD |= (1<<PB0) | (1<<PB1);

weiß ich jetzt auch nicht.

Trotzdem danke für das Problem. Das es hier ein potentielles Problem 
gibt war mir bisher auch nicht bewusst.

von Jonas P. (jox)


Lesenswert?

Karl heinz Buchegger schrieb:

> Das wiedrrum kann nicht sein.
> PORTD ist eine volatile Einheit. Und als solche muss der Compiler jeden
> Zugriff darauf auch durchführen.

Verstehe.

> Es ist schon so, dass dich hier der Optimizer rettet, indem er diesen
> Zugriff durch die Wahl der Assembler Operation atomar macht.

Ja, kann ich bestätigen. Hab es verifiziert.

>
> Was der allerdings macht bei
>
>
>   PORTD |= (1<<PB0) | (1<<PB1);
>
> weiß ich jetzt auch nicht.


Aus
1
PORTD |= (1<<0);
wird
1
sbi  0x0b, 0


Aus
1
PORTD |= (1<<0) | (1<<1);
wird
1
in  r24, 0x0b
2
ori  r24, 0x03
3
out  0x0b, r24


Aus
1
PORTD |= (0<<0);
wird
1
in  r24, 0x0b
2
out  0x0b, r24

(-Os und -O3 machen in allen Fällen kein Unterschied.)

>
> Trotzdem danke für das Problem. Das es hier ein potentielles Problem
> gibt war mir bisher auch nicht bewusst.

Hey, bitte gerne. Ich danke auch. :-)

von Jonas P. (jox)


Lesenswert?

Von mir auch noch eine Randbemerkung:

Auch wenn Konstrukte wie
1
PORTD |= (0<<0);
"sinnfrei" erscheinen, sind sie durch Verwendung von Konventionen 
("value<<bit"-Schreibweise), Makros und Präprozessor doch alltäglich.

Bitte melde dich an um einen Beitrag zu schreiben. Anmeldung ist kostenlos und dauert nur eine Minute.
Bestehender Account
Schon ein Account bei Google/GoogleMail? Keine Anmeldung erforderlich!
Mit Google-Account einloggen
Noch kein Account? Hier anmelden.