Timer-Interrupt mit Schluckauf (ATmega328P)

OP #1846485
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
Persönliche Seite #1846497
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
OP #1846508
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
OP #1846510
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
}
OP #1846520
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!
#1846579
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
#1846665
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
OP #1846686
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
#1846808
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.
OP #1846866
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. :-)

Antwort schreiben

Bitte melde dich an, um einen Beitrag zu schreiben.

oder

Mit Google-Account einloggen

Die Registrierung ist kostenlos und dauert nur eine Minute.

Jetzt registrieren